BREAKING CHANGE: start over for react + three project
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -1,46 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
const config = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
env: { browser: true, es2020: true },
|
||||
plugins: ["@typescript-eslint", "react-refresh"],
|
||||
extends: [
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/recommended-type-checked",
|
||||
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
rules: {
|
||||
// These opinionated rules are enabled in stylistic-type-checked above.
|
||||
// Feel free to reconfigure them to your own preference.
|
||||
"@typescript-eslint/array-type": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
prefer: "type-imports",
|
||||
fixStyle: "inline-type-imports",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
"error",
|
||||
{
|
||||
checksVoidReturn: { attributes: false },
|
||||
},
|
||||
],
|
||||
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
28
.github/workflows/docker-build.yml
vendored
28
.github/workflows/docker-build.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
docker build -t pihkaal/me:latest --build-arg GH_PAT=${{ secrets.GH_PAT }} .
|
||||
docker push pihkaal/me:latest
|
||||
44
.gitignore
vendored
44
.gitignore
vendored
@@ -1,33 +1,29 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# Production
|
||||
/dist
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Debug
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env*.local
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Typescript
|
||||
*.tsbuildinfo
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
# Project
|
||||
.notes
|
||||
|
||||
# Generated
|
||||
/src/assets.ts
|
||||
_old
|
||||
@@ -1,4 +0,0 @@
|
||||
.env.example
|
||||
.idea
|
||||
pnpm-lock.yaml
|
||||
dist
|
||||
@@ -1,8 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
|
||||
const config = {
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
26
Dockerfile
26
Dockerfile
@@ -1,26 +0,0 @@
|
||||
FROM node:20-slim AS base
|
||||
|
||||
ARG GH_PAT
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
ENV GH_PAT="$GH_PAT"
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM base AS build
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2024 Pihkaal <hello@pihkaal.me>
|
||||
Copyright (c) 2025 Pihkaal <hello@pihkaal.me>
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
|
||||
23
build/env.ts
23
build/env.ts
@@ -1,23 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { configDotenv } from "dotenv";
|
||||
|
||||
configDotenv();
|
||||
|
||||
// yeah ok might be overkill lol but I had more env variables before
|
||||
|
||||
const schema = z.object({
|
||||
GH_PAT: z.string().min(1),
|
||||
});
|
||||
|
||||
const result = schema.safeParse(process.env);
|
||||
if (result.success === false) {
|
||||
console.error("❌ Invalid environment variables");
|
||||
console.error(
|
||||
result.error.errors
|
||||
.map((error) => `- ${error.path.join(".")}: ${error.message}`)
|
||||
.join("\n"),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export const env = result.data;
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Plugin } from "vite";
|
||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||
import { spawnSync } from "child_process";
|
||||
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
|
||||
import { env } from "./env";
|
||||
import { existsSync } from "fs";
|
||||
import showdown from "showdown";
|
||||
|
||||
type Manifest = {
|
||||
files: string[];
|
||||
projects: string[];
|
||||
links: {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Project = {
|
||||
name: string;
|
||||
content: string;
|
||||
language: string | null;
|
||||
url: string;
|
||||
private: boolean;
|
||||
};
|
||||
|
||||
type File = {
|
||||
name: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export const manifest = (): Plugin => ({
|
||||
name: "generate-pages-plugin",
|
||||
buildStart: async () => {
|
||||
const octokit = new Octokit({ auth: env.GH_PAT });
|
||||
let manifestRepo: RestEndpointMethodTypes["repos"]["get"]["response"]["data"];
|
||||
|
||||
try {
|
||||
const { data } = await octokit.repos.get({
|
||||
owner: "pihkaal",
|
||||
repo: "pihkaal",
|
||||
});
|
||||
manifestRepo = data;
|
||||
} catch {
|
||||
if (existsSync("./node_modules/.cache/assets")) {
|
||||
console.warn("WARNING: Can't update assets, using cached ones");
|
||||
return;
|
||||
} else {
|
||||
throw new Error("Can't update assets, nothing cached");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const storedUpdatedAt = (
|
||||
await readFile("./node_modules/.cache/assets")
|
||||
).toString();
|
||||
if (storedUpdatedAt === manifestRepo.updated_at) return;
|
||||
} catch {}
|
||||
|
||||
await mkdir("./node_modules/.cache", { recursive: true });
|
||||
await writeFile("./node_modules/.cache/assets", manifestRepo.updated_at);
|
||||
|
||||
const getRepoFileContent = async (repo: string, path: string) => {
|
||||
const { data: file } = await octokit.repos.getContent({
|
||||
owner: "pihkaal",
|
||||
repo,
|
||||
path,
|
||||
});
|
||||
|
||||
if (Array.isArray(file) || file.type !== "file") throw new Error("");
|
||||
|
||||
return Buffer.from(file.content, "base64").toString("utf8");
|
||||
};
|
||||
|
||||
const manifest = JSON.parse(
|
||||
await getRepoFileContent("pihkaal", "manifest.json"),
|
||||
) as Manifest;
|
||||
|
||||
showdown.setFlavor("github");
|
||||
const converter = new showdown.Converter();
|
||||
|
||||
const projects: Array<Project> = [];
|
||||
for (const project of manifest.projects) {
|
||||
const { data: repo } = await octokit.repos.get({
|
||||
owner: "pihkaal",
|
||||
repo: project,
|
||||
});
|
||||
const content = await getRepoFileContent(project, "README.md");
|
||||
let html = converter.makeHtml(content);
|
||||
|
||||
// that's honestly not really clean but it does exactly what i need
|
||||
|
||||
if (!repo.private) {
|
||||
const repoLink = `https://github.com/pihkaal/${project}`;
|
||||
if (html.includes('id="links')) {
|
||||
html = html.replace(
|
||||
'id="links">',
|
||||
`><a href=\"${repoLink}\">Repo</a> •`,
|
||||
);
|
||||
} else {
|
||||
html = html += `<br>\n<a href="${repoLink}">Github repo</>`;
|
||||
}
|
||||
}
|
||||
|
||||
html = html
|
||||
.replace(new RegExp('href="https', "g"), 'target="_blank" href="https')
|
||||
.replace(
|
||||
new RegExp('target="_blank" href="https://pihkaal.me', "g"),
|
||||
'href="#',
|
||||
);
|
||||
|
||||
projects.push({
|
||||
name: project,
|
||||
content: html,
|
||||
language: repo.language,
|
||||
url: repo.url,
|
||||
private: repo.private,
|
||||
});
|
||||
}
|
||||
|
||||
const files: Array<File> = [];
|
||||
for (const file of manifest.files) {
|
||||
const content = await getRepoFileContent("pihkaal", file);
|
||||
const html = converter.makeHtml(content);
|
||||
|
||||
files.push({
|
||||
name: file,
|
||||
content: html,
|
||||
});
|
||||
}
|
||||
|
||||
const code = `
|
||||
const projects = ${JSON.stringify(projects, null, 2)} as const;
|
||||
|
||||
const links = ${JSON.stringify(manifest.links, null, 2)} as const;
|
||||
|
||||
const files = ${JSON.stringify(files, null, 2)} as const;
|
||||
|
||||
export const assets = {
|
||||
projects,
|
||||
links,
|
||||
files
|
||||
};
|
||||
`;
|
||||
|
||||
await writeFile("./src/assets.ts", code);
|
||||
|
||||
spawnSync("prettier", ["--write", "./src/assets.ts"]);
|
||||
},
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
services:
|
||||
pihkaal-me:
|
||||
image: pihkaal/me:latest
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.pihkaal-me.rule=Host(`pihkaal.me`)"
|
||||
- "traefik.http.routers.pihkaal-me.service=pihkaal-me"
|
||||
- "traefik.http.services.pihkaal-me.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.pihkaal-me.tls=true"
|
||||
- "traefik.http.routers.pihkaal-me.tls.certResolver=myresolver"
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
14
index.html
14
index.html
@@ -2,20 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta property="og:title" content="pihkaal.me" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="French passionate, and self-taught developer."
|
||||
/>
|
||||
<meta property="og:image" content="https://i.imgur.com/8xRqYCS.png" />
|
||||
<meta property="og:url" content="https://pihkaal.me" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="theme-color" content="#918cbb" />
|
||||
|
||||
<title>pihkaal</title>
|
||||
<title>pihkaal.me</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
60
package.json
60
package.json
@@ -1,51 +1,35 @@
|
||||
{
|
||||
"name": "pihkaal.me",
|
||||
"version": "0.1.0",
|
||||
"description": "My personal website",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"uncache": "rm -f node_modules/.cache/assets",
|
||||
"build": "rm -f node_modules/.cache/assets && vite build && tsc",
|
||||
"dev": "vite",
|
||||
"lint": "eslint src --cache --fix --max-warnings 0",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint --cache .",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --cache --write ."
|
||||
"format": "prettier --write --cache ."
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "2.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwind-merge": "2.2.1"
|
||||
"@react-three/drei": "^10.0.8",
|
||||
"@react-three/fiber": "^9.1.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"three": "^0.176.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "21.0.0",
|
||||
"@types/eslint": "8.56.1",
|
||||
"@types/node": "18.19.6",
|
||||
"@types/react": "18.2.47",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@vitejs/plugin-react-swc": "3.5.0",
|
||||
"autoprefixer": "10.4.16",
|
||||
"dotenv": "16.4.5",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-next": "14.0.4",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-refresh": "0.4.5",
|
||||
"postcss": "8.4.33",
|
||||
"prettier": "3.1.1",
|
||||
"prettier-plugin-tailwindcss": "0.5.11",
|
||||
"sass": "1.70.0",
|
||||
"showdown": "^2.1.0",
|
||||
"tailwindcss": "3.4.1",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.12",
|
||||
"vite-tsconfig-paths": "4.3.1",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.25.1"
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/three": "^0.176.0",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
4222
pnpm-lock.yaml
generated
4222
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 214 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB |
10
src/App.css
Normal file
10
src/App.css
Normal file
@@ -0,0 +1,10 @@
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
86
src/App.tsx
86
src/App.tsx
@@ -1,60 +1,44 @@
|
||||
import { Kitty } from "./components/Kitty";
|
||||
import { AppProvider } from "./providers/AppProvider";
|
||||
import { Music } from "./components/Music";
|
||||
import { Nvim } from "./components/Nvim";
|
||||
import { Waybar } from "./components/Waybar";
|
||||
import { useApp } from "./hooks/useApp";
|
||||
import { clamp } from "./utils/math";
|
||||
import { Sddm } from "./components/Sddm";
|
||||
import { Boot } from "./components/Boot";
|
||||
import { Off } from "./components/Off";
|
||||
import * as THREE from "three";
|
||||
import { useRef, useState } from "react";
|
||||
import { Canvas, type ThreeElements } from "@react-three/fiber";
|
||||
import { OrbitControls } from '@react-three/drei'
|
||||
|
||||
const AppRoot = () => {
|
||||
const { brightness, state } = useApp();
|
||||
|
||||
const opacity = clamp(0.5 - (0.5 * brightness) / 100, 0, 0.5);
|
||||
|
||||
if (state === "off" || state === "reboot" || state === "suspend") {
|
||||
return <Off />;
|
||||
}
|
||||
|
||||
if (state === "boot") {
|
||||
return <Boot />;
|
||||
}
|
||||
import "./App.css";
|
||||
|
||||
function Box(props: ThreeElements["mesh"]) {
|
||||
const ref = useRef<THREE.Mesh>(null!);
|
||||
const [hovered, hover] = useState(false);
|
||||
const [clicked, click] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none fixed inset-0 z-20 bg-black"
|
||||
style={{ opacity }}
|
||||
/>
|
||||
<main className="h-[100svh] w-screen overflow-hidden bg-[url(/wallpaper.webp)] bg-cover bg-center">
|
||||
{state === "login" ? (
|
||||
<Sddm />
|
||||
) : (
|
||||
<div className="h-full flex-col">
|
||||
<div className="relative z-10 p-1 md:p-2">
|
||||
<Waybar />
|
||||
</div>
|
||||
|
||||
<div className="relative flex h-[calc(100svh-39px)] w-full flex-col md:h-[calc(100svh-50px)]">
|
||||
<Kitty className="w-full flex-1 px-1 pt-1 md:px-2 md:pb-1">
|
||||
<Nvim />
|
||||
</Kitty>
|
||||
|
||||
<Music />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
<mesh
|
||||
{...props}
|
||||
ref={ref}
|
||||
scale={clicked ? 1.5 : 1}
|
||||
onClick={() => click(!clicked)}
|
||||
onPointerOver={() => hover(true)}
|
||||
onPointerOut={() => hover(false)}
|
||||
>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AppProvider>
|
||||
<AppRoot />
|
||||
</AppProvider>
|
||||
<Canvas>
|
||||
<ambientLight intensity={Math.PI / 2} />
|
||||
<spotLight
|
||||
position={[10, 10, 10]}
|
||||
angle={0.15}
|
||||
penumbra={1}
|
||||
decay={0}
|
||||
intensity={Math.PI}
|
||||
/>
|
||||
<pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
|
||||
<Box position={[-1.2, 0, 0]} />
|
||||
<Box position={[1.2, 0, 0]} />
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
|
||||
const LINES = [
|
||||
"Loading Linux...",
|
||||
"Loading initial ramdisk...",
|
||||
"Feeding the cat...",
|
||||
"Cleaning my room...",
|
||||
"Preparing tuna tomato couscous...",
|
||||
"Ready",
|
||||
];
|
||||
|
||||
export const Boot = () => {
|
||||
const { setState } = useApp();
|
||||
const [line, setLine] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (line >= LINES.length) {
|
||||
setState("login");
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(
|
||||
() => setLine(line + 1),
|
||||
line === 0
|
||||
? 3500
|
||||
: line === LINES.length - 1
|
||||
? 1200
|
||||
: Math.random() * 750 + 200,
|
||||
);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [setState, line]);
|
||||
|
||||
return (
|
||||
<main className="h-[100svh] w-screen bg-black text-white">
|
||||
{LINES.filter((_, i) => i <= line).map((line, i) => (
|
||||
<p key={i}>{line}</p>
|
||||
))}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import {
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
useId,
|
||||
type HTMLAttributes,
|
||||
} from "react";
|
||||
import { type KittyContextProps } from "~/context/KittyContext";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
import { KittyProvider } from "~/providers/KittyProvider";
|
||||
|
||||
export const CHAR_WIDTH = 12;
|
||||
export const CHAR_HEIGHT = 26;
|
||||
|
||||
const PADDING_RIGHT = CHAR_WIDTH / 2;
|
||||
|
||||
export const Kitty = (props: {
|
||||
children?: ReactNode;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState<`${number}px` | "auto">(
|
||||
props.cols ? `${props.cols * CHAR_WIDTH}px` : "auto",
|
||||
);
|
||||
const [height, setHeight] = useState<`${number}px` | "auto">(
|
||||
props.rows ? `${props.rows * CHAR_HEIGHT}px` : "auto",
|
||||
);
|
||||
const [context, setContext] = useState<KittyContextProps | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const id = useId();
|
||||
const { activeKitty, setActiveKitty } = useApp();
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setActiveKitty(id);
|
||||
}, [id, setActiveKitty]);
|
||||
|
||||
const snapToCharacter = useCallback(() => {
|
||||
if (!container.current) return;
|
||||
|
||||
const cols = Math.round(
|
||||
(container.current.clientWidth - PADDING_RIGHT) / CHAR_WIDTH,
|
||||
);
|
||||
|
||||
const rows = Math.round(container.current.clientHeight / CHAR_HEIGHT);
|
||||
|
||||
const width = cols * CHAR_WIDTH;
|
||||
const height = rows * CHAR_HEIGHT;
|
||||
|
||||
setWidth(`${width}px`);
|
||||
setHeight(`${height}px`);
|
||||
setContext({ id, rows, cols });
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!container.current) return;
|
||||
|
||||
snapToCharacter();
|
||||
|
||||
window.addEventListener("resize", snapToCharacter);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", snapToCharacter);
|
||||
};
|
||||
}, [snapToCharacter]);
|
||||
|
||||
const style: HTMLAttributes<HTMLDivElement>["style"] = props.rows
|
||||
? { height }
|
||||
: { maxHeight: height, height: "100%" };
|
||||
|
||||
return (
|
||||
<div className={props.className} onMouseEnter={handleMouseEnter}>
|
||||
<div
|
||||
className={
|
||||
"h-full w-full overflow-hidden rounded-lg border-2 border-borderInactive bg-[#262234] bg-background bg-opacity-90 px-[1px] text-lg text-[#cbc7d1] text-foreground shadow-window transition-colors duration-[500ms] ease-out"
|
||||
}
|
||||
style={{
|
||||
lineHeight: `${CHAR_HEIGHT}px`,
|
||||
...(activeKitty === id
|
||||
? {
|
||||
borderColor: "#cdd6f4",
|
||||
animationDuration: "200ms",
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
ref={container}
|
||||
>
|
||||
<div
|
||||
className="whitespace-pre-wrap"
|
||||
style={{ backdropFilter: "blur(2.5px)", width, ...style }}
|
||||
>
|
||||
<KittyProvider value={context}>{props.children}</KittyProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,159 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { type InnerKittyProps } from "~/utils/types";
|
||||
import { CHAR_WIDTH } from "../Kitty";
|
||||
import { useKitty } from "~/hooks/useKitty";
|
||||
|
||||
export const Cava = (props: { animate: boolean }) => {
|
||||
const kitty = useKitty();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid select-none text-[#b2b9d7]"
|
||||
style={{
|
||||
gap: `${CHAR_WIDTH}px`,
|
||||
gridTemplateColumns: `repeat(auto-fill, ${CHAR_WIDTH * 2}px)`,
|
||||
gridTemplateRows: `1fr`,
|
||||
}}
|
||||
>
|
||||
{kitty && <InnerCava {...props} {...kitty} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FrequencyBar = (props: {
|
||||
value: number;
|
||||
max: number;
|
||||
height: number;
|
||||
}) => {
|
||||
const WIDTH = 2;
|
||||
const GRADIENT = "▁▂▃▄▅▆▇█";
|
||||
const FULL_BLOCK = "█";
|
||||
|
||||
const fraction = props.value / props.max;
|
||||
const totalCharacters = props.height * GRADIENT.length;
|
||||
const filledCharacters = fraction * totalCharacters;
|
||||
|
||||
const fullBlocksCount = Math.floor(filledCharacters / GRADIENT.length);
|
||||
const remainderIndex = Math.floor(filledCharacters % GRADIENT.length);
|
||||
|
||||
let bar = "";
|
||||
|
||||
const emptyBlocksCount =
|
||||
props.height - fullBlocksCount - (remainderIndex > 0 ? 1 : 0);
|
||||
if (remainderIndex === 0 && fullBlocksCount === 0) {
|
||||
bar += `${" ".repeat(WIDTH)}\n`.repeat(Math.max(emptyBlocksCount - 1, 0));
|
||||
bar += GRADIENT[0].repeat(WIDTH);
|
||||
} else {
|
||||
bar += `${" ".repeat(WIDTH)}\n`.repeat(Math.max(emptyBlocksCount, 0));
|
||||
|
||||
if (remainderIndex > 0) {
|
||||
bar += `${GRADIENT[remainderIndex].repeat(WIDTH)}\n`;
|
||||
}
|
||||
bar += `${FULL_BLOCK.repeat(WIDTH)}\n`.repeat(fullBlocksCount);
|
||||
}
|
||||
|
||||
return <span>{bar}</span>;
|
||||
};
|
||||
|
||||
const InnerCava = (props: InnerKittyProps<typeof Cava>) => {
|
||||
const sourceRef = useRef<AudioBufferSourceNode | null>(null);
|
||||
const analyserRef = useRef<AnalyserNode | null>(null);
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const dataArray = useRef<Uint8Array | null>(null);
|
||||
const [barHeights, setBarHeights] = useState(
|
||||
new Array<number>(Math.floor(props.cols / 3)).fill(0),
|
||||
);
|
||||
|
||||
const requestRef = useRef<number>();
|
||||
const calculateBarHeights = useCallback(() => {
|
||||
if (!dataArray.current || !analyserRef.current) return;
|
||||
|
||||
analyserRef.current.getByteFrequencyData(dataArray.current);
|
||||
|
||||
const barCount = Math.floor(props.cols / 2);
|
||||
const newBarHeights = [];
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const startIndex = Math.floor((i / barCount) * dataArray.current.length);
|
||||
const endIndex = Math.floor(
|
||||
((i + 1) / barCount) * dataArray.current.length,
|
||||
);
|
||||
const slice = dataArray.current.slice(startIndex, endIndex);
|
||||
const sum = slice.reduce((acc, val) => acc + val, 0);
|
||||
const average = sum / slice.length;
|
||||
newBarHeights.push(average * 0.9);
|
||||
}
|
||||
|
||||
const stateBarHeights =
|
||||
barHeights.length !== newBarHeights.length
|
||||
? new Array<number>(newBarHeights.length).fill(0)
|
||||
: barHeights;
|
||||
|
||||
const smoothedBarHeights = newBarHeights.map((height, i) => {
|
||||
const smoothingFactor = 0.8;
|
||||
return (
|
||||
stateBarHeights[i] + (height - stateBarHeights[i]) * smoothingFactor
|
||||
);
|
||||
});
|
||||
|
||||
setBarHeights(smoothedBarHeights);
|
||||
|
||||
requestRef.current = requestAnimationFrame(calculateBarHeights);
|
||||
}, [barHeights, props.cols]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAudio = async () => {
|
||||
try {
|
||||
const audioContext = new AudioContext();
|
||||
audioContextRef.current = audioContext;
|
||||
|
||||
const response = await fetch("/audio/music.mp3");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
|
||||
const analyserNode = audioContext.createAnalyser();
|
||||
analyserNode.fftSize = 256;
|
||||
|
||||
const gainNode = audioContext.createGain();
|
||||
gainNode.gain.value = 0;
|
||||
|
||||
const source = audioContext.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.loop = true;
|
||||
|
||||
source.connect(analyserNode);
|
||||
analyserNode.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
analyserRef.current = analyserNode;
|
||||
sourceRef.current = source;
|
||||
dataArray.current = new Uint8Array(analyserNode.frequencyBinCount);
|
||||
|
||||
requestRef.current = requestAnimationFrame(calculateBarHeights);
|
||||
|
||||
source.start();
|
||||
} catch (error) {
|
||||
console.error("Error fetching or decoding audio:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (audioContextRef.current) {
|
||||
requestRef.current = requestAnimationFrame(calculateBarHeights);
|
||||
} else {
|
||||
void fetchAudio();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (requestRef.current) cancelAnimationFrame(requestRef.current);
|
||||
};
|
||||
}, [calculateBarHeights]);
|
||||
|
||||
return barHeights.map((height, i) => (
|
||||
<FrequencyBar
|
||||
key={i}
|
||||
value={props.animate ? height : 0}
|
||||
max={255}
|
||||
height={props.rows}
|
||||
/>
|
||||
));
|
||||
};
|
||||
@@ -1,159 +0,0 @@
|
||||
import { formatMMSS } from "../../utils/time";
|
||||
import { CharArray } from "../../utils/string";
|
||||
import { CHAR_HEIGHT, CHAR_WIDTH } from "../Kitty";
|
||||
import { type InnerKittyProps } from "~/utils/types";
|
||||
import { useKitty } from "~/hooks/useKitty";
|
||||
import { type CurrentlyPlaying } from ".";
|
||||
|
||||
export const SpotifyPlayer = (props: { playing: CurrentlyPlaying | null }) => {
|
||||
const kitty = useKitty();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid select-none"
|
||||
style={{
|
||||
gridTemplateColumns: `${CHAR_WIDTH}px ${
|
||||
CHAR_WIDTH * 8
|
||||
}px 1fr ${CHAR_WIDTH}px`,
|
||||
gridTemplateRows: `${CHAR_HEIGHT}px ${
|
||||
CHAR_HEIGHT * 3
|
||||
}px ${CHAR_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{kitty && <InnerSpotifyPlayer {...props} {...kitty} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InnerSpotifyPlayer = (props: InnerKittyProps<typeof SpotifyPlayer>) => {
|
||||
if (props.playing === null) {
|
||||
return (
|
||||
<>
|
||||
{/* title */}
|
||||
<span
|
||||
className="font-extrabold text-color5"
|
||||
style={{ gridArea: "1 / 2 / 2 / 3" }}
|
||||
>
|
||||
Playback
|
||||
</span>
|
||||
|
||||
{/* border right */}
|
||||
<span style={{ gridArea: "1 / 4 / 2 / 5" }}>┐</span>
|
||||
<span style={{ gridArea: "2 / 4 / 3 / 4" }}>
|
||||
{"│\n".repeat(props.rows - 2)}
|
||||
</span>
|
||||
<span style={{ gridArea: "3 / 4 / 4 / 5" }}>┘</span>
|
||||
|
||||
{/* borer left */}
|
||||
<span style={{ gridArea: "1 / 1 / 2 / 2" }}>┌</span>
|
||||
<span style={{ gridArea: "2 / 1 / 3 / 1" }}>
|
||||
{"│\n".repeat(props.rows - 2)}
|
||||
</span>
|
||||
<span style={{ gridArea: "3 / 1 / 4 / 2" }}>└</span>
|
||||
|
||||
{/* border top */}
|
||||
<span className="overflow-hidden" style={{ gridArea: "1 / 3 / 2 / 4" }}>
|
||||
{"─".repeat(props.cols - 2 - 8)}
|
||||
</span>
|
||||
|
||||
{/* border bottom */}
|
||||
<span className="overflow-hidden" style={{ gridArea: "3 / 2 / 4 / 4" }}>
|
||||
{"─".repeat(props.cols - 2)}
|
||||
</span>
|
||||
|
||||
{/* body */}
|
||||
<div className="overflow-hidden" style={{ gridArea: "2 / 2 / 3 / 4" }}>
|
||||
No playback found
|
||||
<br />
|
||||
I'm not listening to anything right now
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const fillSize = Math.round(
|
||||
(props.playing.progress_ms / props.playing.item.duration_ms) *
|
||||
(props.cols - 2),
|
||||
);
|
||||
const emptySize = props.cols - 2 - fillSize;
|
||||
|
||||
const timeString = `${formatMMSS(
|
||||
props.playing.progress_ms / 1000,
|
||||
)}/${formatMMSS(props.playing.item.duration_ms / 1000)}`;
|
||||
const timeStringLeft = Math.round(
|
||||
(props.cols - 2) / 2 - timeString.length / 2,
|
||||
);
|
||||
|
||||
const fill = new CharArray(" ", fillSize)
|
||||
.write(timeStringLeft, timeString)
|
||||
.toString();
|
||||
const empty = new CharArray(" ", emptySize)
|
||||
.write(timeStringLeft - fillSize, timeString)
|
||||
.toString();
|
||||
|
||||
let titleAndArtist = `${
|
||||
props.playing.item.name
|
||||
} · ${props.playing.item.artists.map((a) => a.name).join(", ")}`;
|
||||
if (titleAndArtist.length > props.cols - 4) {
|
||||
titleAndArtist = titleAndArtist.slice(0, props.cols - 7) + "...";
|
||||
}
|
||||
|
||||
let album = props.playing.item.album.name;
|
||||
if (album.length > props.cols - 2) {
|
||||
album = album.slice(0, props.cols - 5) + "...";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* title */}
|
||||
<span
|
||||
className="font-extrabold text-color5"
|
||||
style={{ gridArea: "1 / 2 / 2 / 3" }}
|
||||
>
|
||||
Playback
|
||||
</span>
|
||||
|
||||
{/* border right */}
|
||||
<span style={{ gridArea: "1 / 4 / 2 / 5" }}>┐</span>
|
||||
<span style={{ gridArea: "2 / 4 / 3 / 4" }}>
|
||||
{"│\n".repeat(props.rows - 2)}
|
||||
</span>
|
||||
<span style={{ gridArea: "3 / 4 / 4 / 5" }}>┘</span>
|
||||
|
||||
{/* borer left */}
|
||||
<span style={{ gridArea: "1 / 1 / 2 / 2" }}>┌</span>
|
||||
<span style={{ gridArea: "2 / 1 / 3 / 1" }}>
|
||||
{"│\n".repeat(props.rows - 2)}
|
||||
</span>
|
||||
<span style={{ gridArea: "3 / 1 / 4 / 2" }}>└</span>
|
||||
|
||||
{/* border top */}
|
||||
<span className="overflow-hidden" style={{ gridArea: "1 / 3 / 2 / 4" }}>
|
||||
{"─".repeat(props.cols - 2 - 8)}
|
||||
</span>
|
||||
|
||||
{/* border bottom */}
|
||||
<span className="overflow-hidden" style={{ gridArea: "3 / 2 / 4 / 4" }}>
|
||||
{"─".repeat(props.cols - 2)}
|
||||
</span>
|
||||
|
||||
{/* body */}
|
||||
<div className="overflow-hidden" style={{ gridArea: "2 / 2 / 3 / 4" }}>
|
||||
<span className="font-extrabold text-color6">
|
||||
<span className="font-normal">
|
||||
{false ? "\udb81\udc0a " : "\udb80\udfe4 "}
|
||||
</span>
|
||||
{titleAndArtist}
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-color3">{album}</span>
|
||||
<br />
|
||||
<div className="relative font-extrabold">
|
||||
<span className="bg-color2 text-color8">{fill}</span>
|
||||
<span className="bg-color8 text-color2">{empty}</span>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Kitty } from "../Kitty";
|
||||
import { SpotifyPlayer } from "./SpotifyPlayer";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
import { cn, hideIf } from "~/utils/react";
|
||||
import { Cava } from "./Cava";
|
||||
|
||||
export type CurrentlyPlaying = {
|
||||
is_playing: boolean;
|
||||
item: {
|
||||
album: {
|
||||
name: string;
|
||||
};
|
||||
name: string;
|
||||
artists: { name: string }[];
|
||||
duration_ms: number;
|
||||
};
|
||||
progress_ms: number;
|
||||
};
|
||||
|
||||
export const Music = () => {
|
||||
const { screenWidth } = useApp();
|
||||
const [playing, setPlaying] = useState<CurrentlyPlaying | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentlyPlaying = () =>
|
||||
fetch("https://api.pihkaal.me/currently-playing?format=json")
|
||||
.then((r) => r.json())
|
||||
.then((data: CurrentlyPlaying) => {
|
||||
if (data.is_playing) {
|
||||
data.progress_ms = Math.max(0, data.progress_ms - 1500);
|
||||
setPlaying(data);
|
||||
} else {
|
||||
setPlaying(null);
|
||||
}
|
||||
});
|
||||
|
||||
const updatePlayingInterval = setInterval(() => {
|
||||
void fetchCurrentlyPlaying();
|
||||
}, 1000 * 10);
|
||||
|
||||
const updateTimeInterval = setInterval(() => {
|
||||
setPlaying((prev) => {
|
||||
if (prev === null) return null;
|
||||
|
||||
if (prev.progress_ms >= prev.item.duration_ms) {
|
||||
void fetchCurrentlyPlaying();
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
progress_ms: Math.min(prev.item.duration_ms, prev.progress_ms + 1000),
|
||||
};
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
void fetchCurrentlyPlaying();
|
||||
|
||||
return () => {
|
||||
clearInterval(updateTimeInterval);
|
||||
clearInterval(updatePlayingInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row pb-1">
|
||||
<Kitty
|
||||
className={cn(
|
||||
"h-full pb-1.5 pl-1 pr-1 pt-1 md:px-2",
|
||||
screenWidth < 900 ? "w-full" : "w-1/2",
|
||||
)}
|
||||
rows={5}
|
||||
>
|
||||
<SpotifyPlayer playing={playing} />
|
||||
</Kitty>
|
||||
|
||||
<Kitty
|
||||
className={cn(
|
||||
"h-full w-1/2 pb-2 pl-1 pr-2 pt-1",
|
||||
hideIf(screenWidth < 900),
|
||||
)}
|
||||
rows={5}
|
||||
>
|
||||
<Cava animate={playing !== null} />
|
||||
</Kitty>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
export const NvimEditor = (props: { content: string | undefined }) => {
|
||||
let rows = props.content?.split("\n") ?? [];
|
||||
// trim end empty lines
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
if (rows[i].trim().length === 0) {
|
||||
rows = rows.slice(0, rows.length - 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// add spaces in empty lines
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (rows[i].trim().length === 0) {
|
||||
rows[i] = " ";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div
|
||||
className="plain-html"
|
||||
dangerouslySetInnerHTML={{ __html: props.content ?? "" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export const NvimInput = () => <div>~ hello@pihkaal.me</div>;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { DEFAULT_ICON } from "~/utils/icons";
|
||||
import { type Icon } from "~/utils/tree";
|
||||
|
||||
export const NvimStatusBar = (props: {
|
||||
label: string;
|
||||
labelColor: string;
|
||||
fileIcon?: Icon;
|
||||
fileName?: string;
|
||||
}) => (
|
||||
<div className="select-none bg-[#29293c]">
|
||||
<span className="text-[#272332]" style={{ background: props.labelColor }}>
|
||||
{` ${props.label} `}
|
||||
</span>
|
||||
<span className="text-[#474353]" style={{ background: props.labelColor }}>
|
||||
{"\ue0ba"}
|
||||
</span>
|
||||
<span className="bg-[#474353] text-[#373040]">{"\ue0ba"}</span>
|
||||
<span className="bg-[#373040]">{` ${
|
||||
props.fileIcon?.char ?? DEFAULT_ICON.char
|
||||
}${props.fileName ?? "Empty"} `}</span>
|
||||
<span className="bg-[#373040] text-[#29293c]">{"\ue0ba"}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { DEFAULT_ICON } from "~/utils/icons";
|
||||
import { type Child } from "~/utils/tree";
|
||||
|
||||
export const NvimTreeChild = (props: {
|
||||
child: Child;
|
||||
y: number;
|
||||
selected: boolean;
|
||||
inDirectory: boolean | "last";
|
||||
onSelect: (y: number) => void;
|
||||
onOpen: (file: Child) => void;
|
||||
}) => {
|
||||
const icon = props.child.icon ?? DEFAULT_ICON;
|
||||
const [lastClick, setLastClick] = useState<number>();
|
||||
|
||||
const handleClick = () => {
|
||||
props.onSelect(props.y);
|
||||
|
||||
if (lastClick && Date.now() - lastClick <= 500) {
|
||||
props.onOpen(props.child);
|
||||
}
|
||||
|
||||
setLastClick(Date.now());
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
style={{ background: props.selected ? "#504651" : "" }}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{" "}
|
||||
{props.inDirectory && (
|
||||
<span className="text-[#5b515b]">
|
||||
{props.inDirectory === "last" ? "└ " : "│ "}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ color: icon.color }}>{`${icon.char}`}</span>
|
||||
{props.child.name === "README.md" ? (
|
||||
<span className="font-extrabold text-[#d8c5a1]">README.md</span>
|
||||
) : (
|
||||
<span>{props.child.name}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { type Folder } from "~/utils/tree";
|
||||
|
||||
export const NvimTreeDirectory = (props: {
|
||||
directory: Folder;
|
||||
y: number;
|
||||
selected: boolean;
|
||||
onSelect: (y: number) => void;
|
||||
onOpen: (directory: Folder) => void;
|
||||
}) => {
|
||||
const [lastClick, setLastClick] = useState<number>();
|
||||
|
||||
const handleClick = () => {
|
||||
props.onSelect(props.y);
|
||||
|
||||
if (lastClick && Date.now() - lastClick <= 500) {
|
||||
props.onOpen(props.directory);
|
||||
}
|
||||
|
||||
setLastClick(Date.now());
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
className="text-[#a0b6ee]"
|
||||
style={{ background: props.selected ? "#504651" : "" }}
|
||||
onMouseDown={handleClick}
|
||||
>
|
||||
{props.directory.opened ? (
|
||||
<> </>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-[#716471]"> </span>{" "}
|
||||
</>
|
||||
)}
|
||||
{props.directory.name}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
import { CHAR_HEIGHT, CHAR_WIDTH } from "../../Kitty";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { type InnerKittyProps } from "~/utils/types";
|
||||
import { type Nvim } from "..";
|
||||
import { NvimTreeDirectory } from "./NvimTreeDirectory";
|
||||
import { NvimTreeChild } from "./NvimTreeChild";
|
||||
import { assets } from "~/assets";
|
||||
import {
|
||||
file,
|
||||
folder,
|
||||
project,
|
||||
sortFiles,
|
||||
link,
|
||||
type Child,
|
||||
} from "~/utils/tree";
|
||||
|
||||
const buildTree = () =>
|
||||
sortFiles([
|
||||
folder(
|
||||
"links",
|
||||
assets.links.map((l) => link(l.name, l.url, l.icon)),
|
||||
),
|
||||
folder(
|
||||
"projects",
|
||||
assets.projects.map((p) =>
|
||||
project(p.name, p.content, p.url, p.language, p.private),
|
||||
),
|
||||
),
|
||||
...assets.files.map((f) => file(f.name, f.content)),
|
||||
]);
|
||||
|
||||
export const NvimTree = (
|
||||
props: InnerKittyProps<typeof Nvim> & {
|
||||
onOpen: (file: Child) => void;
|
||||
},
|
||||
) => {
|
||||
const { activeKitty } = useApp();
|
||||
|
||||
const [files, setFiles] = useState(buildTree());
|
||||
const [selectedY, setSelectedY] = useState(0);
|
||||
|
||||
const tree: Array<ReactNode> = [];
|
||||
let y = 0;
|
||||
let selectedFile = files[0];
|
||||
for (const file of files) {
|
||||
if (selectedY === y) selectedFile = file;
|
||||
if (file.type === "folder") {
|
||||
tree.push(
|
||||
<NvimTreeDirectory
|
||||
key={y}
|
||||
directory={file}
|
||||
y={y}
|
||||
selected={selectedY === y}
|
||||
onSelect={setSelectedY}
|
||||
onOpen={(directory) => {
|
||||
directory.opened = !directory.opened;
|
||||
setFiles([...files]);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
if (file.opened) {
|
||||
file.children.forEach((child, i) => {
|
||||
y++;
|
||||
if (selectedY === y) selectedFile = child;
|
||||
tree.push(
|
||||
<NvimTreeChild
|
||||
key={y}
|
||||
child={child}
|
||||
y={y}
|
||||
inDirectory={i === file.children.length - 1 ? "last" : true}
|
||||
selected={selectedY === y}
|
||||
onSelect={setSelectedY}
|
||||
onOpen={props.onOpen}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
tree.push(
|
||||
<NvimTreeChild
|
||||
key={y}
|
||||
child={file}
|
||||
y={y}
|
||||
inDirectory={false}
|
||||
selected={selectedY === y}
|
||||
onSelect={setSelectedY}
|
||||
onOpen={props.onOpen}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
y++;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const readme = files.find((file) => file.name === "README.md") as Child;
|
||||
props.onOpen(readme);
|
||||
setSelectedY(files.indexOf(readme));
|
||||
|
||||
const onScroll = (event: KeyboardEvent) => {
|
||||
if (activeKitty !== props.id) return;
|
||||
switch (event.key) {
|
||||
case "ArrowUp":
|
||||
setSelectedY((x) => Math.max(0, x - 1));
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
setSelectedY((x) => Math.min(y - 1, x + 1));
|
||||
break;
|
||||
|
||||
case "Enter":
|
||||
if (selectedFile.type === "folder") {
|
||||
selectedFile.opened = !selectedFile.opened;
|
||||
setFiles([...files]);
|
||||
} else {
|
||||
props.onOpen(selectedFile);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full select-none bg-[#0000001a]">
|
||||
<ul
|
||||
style={{
|
||||
padding: `${CHAR_HEIGHT}px ${CHAR_WIDTH}px 0 ${CHAR_WIDTH * 2}px`,
|
||||
}}
|
||||
>
|
||||
{tree}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useKitty } from "~/hooks/useKitty";
|
||||
import { CHAR_HEIGHT, CHAR_WIDTH } from "../Kitty";
|
||||
import { NvimEditor } from "./NvimEditor";
|
||||
import { NvimInput } from "./NvimInput";
|
||||
import { NvimStatusBar } from "./NvimStatusBar";
|
||||
import { NvimTree } from "./NvimTree";
|
||||
import { useState } from "react";
|
||||
import { type InnerKittyProps } from "~/utils/types";
|
||||
import { type Child, type Icon } from "~/utils/tree";
|
||||
|
||||
export const Nvim = (_props: unknown) => {
|
||||
const kitty = useKitty();
|
||||
|
||||
return kitty && <InnerNvimTree {...kitty} />;
|
||||
};
|
||||
|
||||
const InnerNvimTree = (props: InnerKittyProps<typeof Nvim>) => {
|
||||
const [activeChild, setActiveChild] = useState<{
|
||||
name: string;
|
||||
content: string;
|
||||
icon: Icon;
|
||||
}>();
|
||||
|
||||
const handleOpenChild = (child: Child) => {
|
||||
if (child.type === "link") {
|
||||
window.open(child.url, "_blank")?.focus();
|
||||
} else {
|
||||
setActiveChild(child);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid h-full"
|
||||
style={{
|
||||
gridTemplateColumns: `minmax(${CHAR_WIDTH * 20}px, ${
|
||||
Math.round(props.cols * 0.2) * CHAR_WIDTH
|
||||
}px) 1fr`,
|
||||
gridTemplateRows: `1fr ${CHAR_HEIGHT}px ${CHAR_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<div style={{ gridArea: "1 / 1 / 1 / 2" }}>
|
||||
<NvimTree {...props} onOpen={handleOpenChild} />
|
||||
</div>
|
||||
<div
|
||||
className="scrollbar overflow-y-scroll"
|
||||
style={{
|
||||
gridArea: "1 / 2 / 1 / 3",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<NvimEditor content={activeChild?.content} />
|
||||
</div>
|
||||
<div style={{ gridArea: "2 / 1 / 2 / 3" }}>
|
||||
<NvimStatusBar
|
||||
label=" NORMAL"
|
||||
labelColor="#7ea7ca"
|
||||
fileIcon={activeChild?.icon}
|
||||
fileName={activeChild?.name}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ gridArea: "3 / 1 / 3 / 3" }}>
|
||||
<NvimInput />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
|
||||
export const Off = () => {
|
||||
const { state, setState } = useApp();
|
||||
const [booting, setBooting] = useState(state === "reboot");
|
||||
|
||||
useEffect(() => {
|
||||
if (booting) {
|
||||
const timout = setTimeout(() => {
|
||||
if (state === "suspend") {
|
||||
setState("login");
|
||||
} else {
|
||||
setState("boot");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timout);
|
||||
}
|
||||
}, [state, setState, booting]);
|
||||
|
||||
return (
|
||||
<div className="flex h-[100svh] w-screen flex-col items-center justify-center bg-black">
|
||||
<button
|
||||
className={`drop-shadow-white cursor-pointer transition-all ${
|
||||
booting ? "animate-disappear" : "animate-breathing"
|
||||
}`}
|
||||
onClick={() => setBooting(true)}
|
||||
>
|
||||
<svg viewBox="0 0 34 34" width="128">
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M 14 1 L 14 13 L 15 13 L 15 1 L 14 1 z M 19 1 L 19 13 L 20 13 L 20 1 L 19 1 z M 9 3.1855469 C 4.1702837 5.9748853 1.0026451 11.162345 1 17 C 1 25.836556 8.163444 33 17 33 C 25.836556 33 33 25.836556 33 17 C 32.99593 11.163669 29.828666 5.9780498 25 3.1894531 L 25 4.3496094 C 29.280842 7.0494632 31.988612 11.788234 32 17 C 32 25.284271 25.284271 32 17 32 C 8.7157288 32 2 25.284271 2 17 C 2.0120649 11.788824 4.7195457 7.0510246 9 4.3515625 L 9 3.1855469 z "
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<p className="absolute bottom-2 left-1/2 -translate-x-1/2 text-sm text-gray-400 text-white">
|
||||
This website is primarly made for desktop
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,188 +0,0 @@
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
|
||||
const SddmActionButton = (props: {
|
||||
icon: ReactNode;
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<button
|
||||
className="flex select-none flex-col items-center text-white transition-colors hover:text-zinc-800"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.icon}
|
||||
<span>{props.text}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
const PASSWORD_LENGTH = 12;
|
||||
|
||||
export const Sddm = () => {
|
||||
const { setState } = useApp();
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [password, setPassword] = useState(0);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [now, setNow] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setNow(new Date());
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (password >= PASSWORD_LENGTH) {
|
||||
passwordInputRef.current?.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
passwordInputRef.current?.focus();
|
||||
|
||||
const canType =
|
||||
password < 4 ||
|
||||
password === PASSWORD_LENGTH - 1 ||
|
||||
Math.random() > 0.15;
|
||||
setPassword(Math.max(0, password + (canType ? 1 : -1)));
|
||||
},
|
||||
password === 0 ? 3000 : Math.random() * 250 + 100,
|
||||
);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [password]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-none absolute inset-0 z-10 animate-fadeOut bg-black" />
|
||||
|
||||
<div className="flex h-full cursor-default items-center justify-around text-white">
|
||||
<div className="flex h-full w-full flex-col justify-center backdrop-blur-2xl sm:w-2/3 md:w-1/2 lg:w-2/5">
|
||||
<div className="flex flex h-4/5 flex-col items-center justify-between">
|
||||
<div className="text-center">
|
||||
<p className="text-6xl leading-10">Welcome!</p>
|
||||
<p className="text-5xl">
|
||||
{now.toLocaleTimeString("en-us", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
<p>{now.toLocaleDateString("en-us", { dateStyle: "long" })}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-[210px] flex-col gap-5 lg:w-1/2">
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute inset-y-0 flex items-center ps-3.5">
|
||||
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full rounded-full border border-white bg-transparent p-2 text-center text-white placeholder-zinc-200"
|
||||
value={"Pihkaal"}
|
||||
readOnly
|
||||
spellCheck={false}
|
||||
maxLength={15}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute inset-y-0 flex items-center ps-3.5">
|
||||
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
ref={passwordInputRef}
|
||||
readOnly
|
||||
className="block w-full rounded-full border border-white bg-transparent p-2 text-center text-white placeholder-zinc-200"
|
||||
value={
|
||||
showPassword
|
||||
? password > 0
|
||||
? "no."
|
||||
: ""
|
||||
: "•".repeat(password)
|
||||
}
|
||||
placeholder="Password"
|
||||
spellCheck={false}
|
||||
maxLength={PASSWORD_LENGTH}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="flex select-none items-center gap-2 transition-colors hover:border-black hover:text-black">
|
||||
<div
|
||||
className={`relative flex h-3.5 w-3.5 items-center justify-center border border-current`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer h-2 w-2 appearance-none checked:bg-current"
|
||||
checked={showPassword}
|
||||
onChange={() => setShowPassword((show) => !show)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm">Show Password</p>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-[210px] flex-col gap-1 text-left transition-colors lg:w-1/2">
|
||||
<button
|
||||
disabled={password !== PASSWORD_LENGTH}
|
||||
onClick={() => setState("desktop")}
|
||||
className="w-full select-none rounded-full bg-neutral-800 p-2 hover:bg-zinc-800 disabled:cursor-default disabled:bg-white disabled:bg-opacity-30"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<p className="text-sm">Session: Hyprland</p>
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<SddmActionButton
|
||||
key="suspend"
|
||||
onClick={() => setState("suspend")}
|
||||
icon={
|
||||
<svg viewBox="0 0 34 34" width="38">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M 17,1 C 8.163444,1 1,8.163444 1,17 1,25.836556 8.163444,33 17,33 25.836556,33 33,25.836556 33,17 33,8.163444 25.836556,1 17,1 Z m 0,1 C 25.284271,2 32,8.7157288 32,17 32,25.284271 25.284271,32 17,32 8.7157288,32 2,25.284271 2,17 2,8.7157288 8.7157288,2 17,2 Z m -4,9 v 12 h 1 V 11 Z m 7,0 v 12 h 1 V 11 Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
text="Suspend"
|
||||
/>
|
||||
|
||||
<SddmActionButton
|
||||
key="reboot"
|
||||
onClick={() => setState("reboot")}
|
||||
icon={
|
||||
<svg width="38" viewBox="0 0 34 34">
|
||||
<g transform="matrix(1.000593,0,0,1.0006688,0.99050505,-287.73702)">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M 19.001953,1.1308594 V 2 H 19 v 11 h 1 V 2.3359375 A 15,15 45 0 1 32,17 15,15 45 0 1 21.001953,31.455078 v 1.033203 A 16.009488,16.010701 45 0 0 33.009766,17 16.009488,16.010701 45 0 0 19.001953,1.1308594 Z M 12.998047,1.5117188 A 16.009488,16.010701 45 0 0 0.99023438,17 16.009488,16.010701 45 0 0 14.998047,32.869141 V 32 H 15 V 21 H 14 V 31.664062 A 15,15 45 0 1 2,17 15,15 45 0 1 12.998047,2.5449219 Z"
|
||||
transform="matrix(0.70668771,-0.70663419,0.70668771,0.70663419,-8.0273788,304.53335)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
text="Reboot"
|
||||
/>
|
||||
<SddmActionButton
|
||||
key="shutdown"
|
||||
onClick={() => setState("off")}
|
||||
icon={
|
||||
<svg viewBox="0 0 34 34" width="38">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M 14 1 L 14 13 L 15 13 L 15 1 L 14 1 z M 19 1 L 19 13 L 20 13 L 20 1 L 19 1 z M 9 3.1855469 C 4.1702837 5.9748853 1.0026451 11.162345 1 17 C 1 25.836556 8.163444 33 17 33 C 25.836556 33 33 25.836556 33 17 C 32.99593 11.163669 29.828666 5.9780498 25 3.1894531 L 25 4.3496094 C 29.280842 7.0494632 31.988612 11.788234 32 17 C 32 25.284271 25.284271 32 17 32 C 8.7157288 32 2 25.284271 2 17 C 2.0120649 11.788824 4.7195457 7.0510246 9 4.3515625 L 9 3.1855469 z "
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
text="Shutdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useState, type ReactNode, type MouseEvent, useRef } from "react";
|
||||
import { cn } from "~/utils/react";
|
||||
|
||||
export const WaybarWidget = (props: {
|
||||
className?: string;
|
||||
tooltip?: ReactNode;
|
||||
interactable?: boolean;
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
const [tooltipPosition, setTooltipPosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>();
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setTooltipVisible(true);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleMouveMove = (e: MouseEvent) => {
|
||||
if (!tooltipVisible) {
|
||||
setTooltipPosition({ x: e.clientX, y: e.clientY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = undefined;
|
||||
setTooltipVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative py-[6.5px] font-bold text-[#2b2b2c] opacity-90",
|
||||
props.className,
|
||||
props.interactable && "cursor-pointer",
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouveMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
{props.tooltip && tooltipPosition && tooltipVisible && (
|
||||
<div
|
||||
className="fixed z-20 -translate-x-1/2 whitespace-pre-line rounded-[10px] border-2 border-[#11111b] bg-[#e7e7ec] px-3 py-3 font-extrabold text-[#2b2b2c]"
|
||||
style={{ top: tooltipPosition.y + 20, left: tooltipPosition.x }}
|
||||
>
|
||||
{props.tooltip}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { cn } from "~/utils/react";
|
||||
|
||||
export const WaybarWidgetGroup = (props: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
`flex flex-row justify-between rounded-[10px] bg-[#e6e7ec] bg-opacity-80`,
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
import { lerpIcon } from "~/utils/icons";
|
||||
|
||||
const ICONS = ["", "", "", "", "", "", "", "", "", "", ""];
|
||||
|
||||
export const WaybarBatteryWidget = (props: { frequency: number }) => {
|
||||
const [battery] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// setBattery((x) => x - 1);
|
||||
|
||||
if (battery - 1 === 0) {
|
||||
// TODO: do something
|
||||
}
|
||||
}, props.frequency);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
const tooltip =
|
||||
battery === 100
|
||||
? "Full"
|
||||
: battery >= 70
|
||||
? "Almost full"
|
||||
: battery >= 50
|
||||
? "Halfway down, but still doing great. I wonder what happens if the battery reaches 0"
|
||||
: battery >= 25
|
||||
? "Uh maybe you should consider charging me ?"
|
||||
: battery >= 15
|
||||
? "It's really reaching low level now"
|
||||
: battery >= 5
|
||||
? "Are you ignoring my messages ??"
|
||||
: "I warned you";
|
||||
|
||||
return (
|
||||
<WaybarWidget
|
||||
className="pl-[0.625rem] pr-3 text-[#1d7715]"
|
||||
tooltip={tooltip}
|
||||
>
|
||||
<span className="font-normal">{lerpIcon(ICONS, battery, 100)}</span>{" "}
|
||||
{battery}%
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { type WheelEvent } from "react";
|
||||
import { clamp } from "~/utils/math";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
import { lerpIcon } from "~/utils/icons";
|
||||
|
||||
const ICONS = ["", "", ""];
|
||||
|
||||
export const WaybarBrightnessWidget = () => {
|
||||
const { brightness, setBrightness } = useApp();
|
||||
|
||||
const handleScroll = (e: WheelEvent) => {
|
||||
let newBrightness = brightness - Math.sign(e.deltaY);
|
||||
newBrightness = clamp(newBrightness, 0, 100);
|
||||
|
||||
setBrightness(newBrightness);
|
||||
};
|
||||
|
||||
const tooltip =
|
||||
brightness === 100
|
||||
? "Flashbang mode"
|
||||
: brightness >= 70
|
||||
? "Bright as a Brest day!"
|
||||
: brightness >= 50
|
||||
? "Halfway to becoming a night owl"
|
||||
: brightness >= 25
|
||||
? "I'm scared of the dark please stop"
|
||||
: brightness >= 15
|
||||
? "Just turn off you screen at this point"
|
||||
: brightness >= 5
|
||||
? "Can you even read now ?"
|
||||
: "So dark";
|
||||
|
||||
return (
|
||||
<WaybarWidget className="pl-3 pr-[0.625rem]" tooltip={tooltip}>
|
||||
<span onWheel={handleScroll}>
|
||||
<span className="font-normal">{lerpIcon(ICONS, brightness, 100)}</span>{" "}
|
||||
{brightness}%
|
||||
</span>
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
import { clamp, randomMinMax } from "~/utils/math";
|
||||
|
||||
export const WaybarCPUWidget = (props: {
|
||||
cores: number;
|
||||
min: number;
|
||||
max: number;
|
||||
variation: number;
|
||||
frequency: number;
|
||||
}) => {
|
||||
const [usage, setUsage] = useState(
|
||||
new Array<number>(props.cores)
|
||||
.fill(0)
|
||||
.map((_) => randomMinMax(props.min, props.max)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const variation = randomMinMax(-props.variation, props.variation + 1);
|
||||
const index = randomMinMax(0, usage.length);
|
||||
usage[index] = clamp(usage[index] + variation, props.min, props.max);
|
||||
|
||||
setUsage([...usage]);
|
||||
}, props.frequency);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [usage, props.variation, props.min, props.max, props.frequency]);
|
||||
|
||||
const totalUsage = Math.round(
|
||||
usage.reduce((acc, v) => acc + v, 0) / usage.length,
|
||||
);
|
||||
|
||||
return (
|
||||
<WaybarWidget
|
||||
className="pl-3 pr-[0.625rem]"
|
||||
tooltip={
|
||||
<ul>
|
||||
<li>Total: {totalUsage}%</li>
|
||||
{usage.map((value, i) => (
|
||||
<li key={i}>
|
||||
Core{i}: {value}%
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<span className="font-normal"></span> {totalUsage}%
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
import { randomMinMax } from "~/utils/math";
|
||||
|
||||
// TODO: find a good idea to determine disk usage
|
||||
|
||||
export const WaybarDiskWidget = (props: {
|
||||
current: number;
|
||||
variation: number;
|
||||
capacity: number;
|
||||
}) => {
|
||||
const [value] = useState(
|
||||
props.current + randomMinMax(-props.variation, props.variation + 1),
|
||||
);
|
||||
const usage = Math.round((value / props.capacity) * 100);
|
||||
|
||||
return (
|
||||
<WaybarWidget
|
||||
className="pl-[0.625rem] pr-3"
|
||||
tooltip={`SSD - ${value.toFixed(1)}GB used out of ${
|
||||
props.capacity
|
||||
}GiB on / (${usage}%)`}
|
||||
>
|
||||
<span className="font-normal"></span> {usage}%
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarHomeWidget = () => (
|
||||
<WaybarWidget className="text-[#407cdd]">
|
||||
<span className="font-normal"></span>
|
||||
</WaybarWidget>
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarLockWidget = () => {
|
||||
const { setState } = useApp();
|
||||
|
||||
return (
|
||||
<WaybarWidget
|
||||
className="pl-3 pr-[0.625rem]"
|
||||
onClick={() => setState("login")}
|
||||
interactable
|
||||
>
|
||||
<span className="font-normal"></span>
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import { type WheelEvent, useState } from "react";
|
||||
import { clamp } from "~/utils/math";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarMicrophoneWidget = () => {
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [volume, setVolume] = useState(100);
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
let newVolume = volume - Math.sign(e.deltaY) * 5;
|
||||
newVolume = clamp(newVolume, 0, 100);
|
||||
|
||||
setVolume(newVolume);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setMuted((muted) => !muted);
|
||||
};
|
||||
|
||||
const icon = muted ? "" : "";
|
||||
|
||||
const tooltip =
|
||||
volume === 0 || muted
|
||||
? "Don't worry I'm not listening to you"
|
||||
: volume === 100
|
||||
? "Broadcasting loud and clear!"
|
||||
: volume >= 50
|
||||
? "Your voice sounds really great!"
|
||||
: volume >= 20
|
||||
? "I can still hear you, just a bit quieter"
|
||||
: "I can barely hear you anymore :(";
|
||||
|
||||
return (
|
||||
<WaybarWidget className="pl-[0.625rem] pr-3" interactable tooltip={tooltip}>
|
||||
<span
|
||||
className="text-[#ad6bfd]"
|
||||
onWheel={handleWheel}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="font-normal">{icon}</span> {!muted && `${volume}%`}
|
||||
</span>
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarPowerWidget = () => {
|
||||
const { setState } = useApp();
|
||||
|
||||
return (
|
||||
<WaybarWidget
|
||||
className="pl-[0.625rem] pr-3"
|
||||
onClick={() => setState("off")}
|
||||
interactable
|
||||
>
|
||||
<span className="font-normal"></span>
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
import { clamp, randomMinMax } from "~/utils/math";
|
||||
|
||||
// start, min, max and variation are in %
|
||||
// capacity is in Gb
|
||||
|
||||
export const WaybarRAMWidget = (props: {
|
||||
start: number;
|
||||
min: number;
|
||||
max: number;
|
||||
variation: number;
|
||||
frequency: number;
|
||||
capacity: number;
|
||||
}) => {
|
||||
const [usage, setUsage] = useState(props.start);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const offset = randomMinMax(-props.variation, props.variation + 1);
|
||||
setUsage((x) => clamp(x + offset, props.min, props.max));
|
||||
}, props.frequency);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
const used = (usage / 100) * props.capacity;
|
||||
|
||||
// TODO: tooltip
|
||||
// Memory - (capacity * usage).1f GB used
|
||||
return (
|
||||
<WaybarWidget
|
||||
className="px-[0.625rem]"
|
||||
tooltip={`Memory - ${used.toFixed(1)}GB used`}
|
||||
>
|
||||
<span className="font-normal"></span> {usage}%
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
import { clamp, randomMinMax } from "~/utils/math";
|
||||
|
||||
export const WaybarTemperatureWidget = (props: {
|
||||
min: number;
|
||||
max: number;
|
||||
variation: number;
|
||||
frequency: number;
|
||||
}) => {
|
||||
const [temperature, setTemperature] = useState(
|
||||
randomMinMax(props.min, props.max),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const offset = randomMinMax(-props.variation, props.variation + 1);
|
||||
setTemperature((x) => clamp(x + offset, props.min, props.max));
|
||||
}, props.frequency);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
return (
|
||||
<WaybarWidget
|
||||
className="pl-3 pr-[0.625rem]"
|
||||
tooltip="All good until I start playing btd6"
|
||||
>
|
||||
<span className="font-normal"></span> {temperature}°C
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarTimeWidget = () => {
|
||||
const [date, setDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
|
||||
const time = date.toLocaleString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
return <WaybarWidget className="px-[0.625rem]">{time}</WaybarWidget>;
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarTitleWidget = () => (
|
||||
<WaybarWidget className="px-3">pihkaal</WaybarWidget>
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarToggleThemeWidget = () => (
|
||||
<WaybarWidget className="pl-[0.625rem] pr-3" interactable>
|
||||
<span className="font-normal"></span>
|
||||
</WaybarWidget>
|
||||
);
|
||||
@@ -1,46 +0,0 @@
|
||||
import { type WheelEvent, useState } from "react";
|
||||
import { clamp } from "~/utils/math";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
import { lerpIcon } from "~/utils/icons";
|
||||
|
||||
const ICONS = ["", "", ""];
|
||||
|
||||
export const WaybarVolumeWidget = () => {
|
||||
const [muted, setMuted] = useState(false);
|
||||
const { volume, setVolume } = useApp();
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
let newVolume = volume - Math.sign(e.deltaY) * 5;
|
||||
newVolume = clamp(newVolume, 0, 100);
|
||||
|
||||
setVolume(newVolume);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setMuted(!muted);
|
||||
};
|
||||
|
||||
const icon = muted ? "" : lerpIcon(ICONS, volume, 100);
|
||||
|
||||
const toolip =
|
||||
volume === 0 || muted
|
||||
? "You don't like the music? :("
|
||||
: volume === 100
|
||||
? "Always maximum volume when it's Hysta"
|
||||
: volume >= 50
|
||||
? "Turning up the vibes !"
|
||||
: "Enjoying music at a moderate level";
|
||||
|
||||
return (
|
||||
<WaybarWidget className="px-[0.625rem]" interactable tooltip={toolip}>
|
||||
<span
|
||||
className="text-[#407cdd]"
|
||||
onWheel={handleWheel}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="font-normal">{icon}</span> {!muted && `${volume}%`}
|
||||
</span>
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { WaybarWidget } from "../WaybarWidget";
|
||||
|
||||
export const WaybarWeatherWidget = () => {
|
||||
const [temperature, setTemperature] = useState<string>("??");
|
||||
|
||||
useEffect(() => {
|
||||
void fetch("https://wttr.in/Brest?format=j1")
|
||||
.then((response) => response.json())
|
||||
.then((data: { current_condition: Array<{ temp_C: string }> }) =>
|
||||
setTemperature(data.current_condition[0].temp_C),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const hours = new Date().getHours();
|
||||
|
||||
return (
|
||||
<WaybarWidget className="px-[0.625rem]">
|
||||
{hours > 22 || hours < 7 ? "🌙" : "☀️"} {temperature}°
|
||||
</WaybarWidget>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { WaybarWidgetGroup } from "./WaybarWidgetGroup";
|
||||
import { WaybarCPUWidget } from "./Widgets/WaybarCPUWidget";
|
||||
import { WaybarDiskWidget } from "./Widgets/WaybarDiskWidget";
|
||||
import { WaybarRAMWidget } from "./Widgets/WaybarRAMWidget";
|
||||
import { WaybarTitleWidget } from "./Widgets/WaybarTitleWidget";
|
||||
import { WaybarHomeWidget } from "./Widgets/WaybarHomeWidget";
|
||||
import { randomMinMax } from "~/utils/math";
|
||||
import { WaybarTemperatureWidget } from "./Widgets/WaybarTemperatureWidget";
|
||||
import { WaybarBatteryWidget } from "./Widgets/WaybarBatteryWidget";
|
||||
import { WaybarBrightnessWidget } from "./Widgets/WaybarBrightnessWidget";
|
||||
import { WaybarVolumeWidget } from "./Widgets/WaybarVolumeWidget";
|
||||
import { WaybarMicrophoneWidget } from "./Widgets/WaybarMicrophoneWidget";
|
||||
import { WaybarLockWidget } from "./Widgets/WaybarLockWidget";
|
||||
import { WaybarTimeWidget } from "./Widgets/WaybarTimeWidget";
|
||||
import { WaybarPowerWidget } from "./Widgets/WaybarPowerWidget";
|
||||
import { WaybarToggleThemeWidget } from "./Widgets/WaybarToggleThemeWidget";
|
||||
import { WaybarWeatherWidget } from "./Widgets/WaybarWeatherWidget";
|
||||
import { cn, hideIf } from "~/utils/react";
|
||||
import { useApp } from "~/hooks/useApp";
|
||||
|
||||
export const Waybar = () => {
|
||||
const { screenWidth } = useApp();
|
||||
|
||||
return (
|
||||
<div className="grid h-[37px] w-full select-none grid-cols-[1fr_max-content_1fr] grid-rows-1 gap-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<WaybarWidgetGroup className="rounded-r-[5px] pl-[10px] pr-[14px]">
|
||||
<WaybarHomeWidget />
|
||||
</WaybarWidgetGroup>
|
||||
|
||||
<WaybarWidgetGroup className={cn(hideIf(screenWidth < 705))}>
|
||||
<WaybarCPUWidget
|
||||
variation={1}
|
||||
frequency={3250 + randomMinMax(-100, 100)}
|
||||
cores={12}
|
||||
min={8}
|
||||
max={16}
|
||||
/>
|
||||
<WaybarRAMWidget
|
||||
variation={1}
|
||||
frequency={5000 + randomMinMax(-100, 100)}
|
||||
min={18}
|
||||
max={40}
|
||||
start={1 + randomMinMax(20, 30)}
|
||||
capacity={16}
|
||||
/>
|
||||
<WaybarDiskWidget current={35.9} variation={4.1} capacity={160.3} />
|
||||
</WaybarWidgetGroup>
|
||||
|
||||
<WaybarWidgetGroup className={cn(hideIf(screenWidth < 910))}>
|
||||
<WaybarTitleWidget />
|
||||
</WaybarWidgetGroup>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<WaybarWidgetGroup>
|
||||
<WaybarLockWidget />
|
||||
<WaybarTimeWidget />
|
||||
<WaybarPowerWidget />
|
||||
</WaybarWidgetGroup>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<WaybarWidgetGroup className={cn(hideIf(screenWidth < 1320))}>
|
||||
<WaybarTemperatureWidget
|
||||
min={50}
|
||||
max={70}
|
||||
variation={1}
|
||||
frequency={7000 + randomMinMax(-100, 100)}
|
||||
/>
|
||||
|
||||
<WaybarBatteryWidget frequency={7000 + randomMinMax(-100, 100)} />
|
||||
</WaybarWidgetGroup>
|
||||
|
||||
<WaybarWidgetGroup className={cn(hideIf(screenWidth < 890))}>
|
||||
<WaybarBrightnessWidget />
|
||||
<WaybarVolumeWidget />
|
||||
<WaybarMicrophoneWidget />
|
||||
</WaybarWidgetGroup>
|
||||
|
||||
<WaybarWidgetGroup className={cn(hideIf(screenWidth < 490))}>
|
||||
<WaybarWeatherWidget />
|
||||
<WaybarToggleThemeWidget />
|
||||
</WaybarWidgetGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createContext } from "react";
|
||||
import { type Prettify, type State } from "~/utils/types";
|
||||
|
||||
export const AppContext = createContext<AppContextProps | undefined>(undefined);
|
||||
|
||||
export type AppContextProps = Prettify<
|
||||
State<"state", "off" | "suspend" | "reboot" | "boot" | "login" | "desktop"> &
|
||||
State<"activeKitty", string> &
|
||||
State<"brightness", number> &
|
||||
State<"volume", number> & { screenWidth: number }
|
||||
>;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const KittyContext = createContext<KittyContextProps | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export type KittyContextProps = {
|
||||
rows: number;
|
||||
cols: number;
|
||||
id: string;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "~/context/AppContext";
|
||||
|
||||
export const useApp = () => {
|
||||
const app = useContext(AppContext);
|
||||
if (!app) throw new Error("`useApp` used outside AppContext");
|
||||
|
||||
return app;
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { KittyContext } from "~/context/KittyContext";
|
||||
|
||||
export const useKitty = () => useContext(KittyContext);
|
||||
68
src/index.css
Normal file
68
src/index.css
Normal file
@@ -0,0 +1,68 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
120
src/index.scss
120
src/index.scss
@@ -1,120 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: mono;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@import url(https://fonts.bunny.net/css?family=jetbrains-mono:700,800);
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro",
|
||||
"IBM Plex Mono", "Menlo", "DejaVu Sans Mono", "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
*::selection {
|
||||
background-color: #58515c;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src:
|
||||
url("/fonts/JetBrainsMonoNF-Medium.woff2") format("woff2"),
|
||||
url("/fonts/JetBrainsMonoNF-Medium.woff") format("woff");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.drop-shadow-white:hover {
|
||||
filter: drop-shadow(0px 0px 4px rgba(255, 255, 255, 1));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #f7ddd9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #f7ddd9 transparent;
|
||||
}
|
||||
|
||||
.plain-html {
|
||||
@apply w-full whitespace-normal px-10 py-1 xl:w-2/3 2xl:w-3/5;
|
||||
|
||||
& img {
|
||||
@apply inline;
|
||||
}
|
||||
|
||||
& p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
@apply m-[revert];
|
||||
font-size: revert;
|
||||
font-weight: revert;
|
||||
}
|
||||
|
||||
& p {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
@apply text-[2rem] font-bold;
|
||||
|
||||
& img {
|
||||
@apply mb-4;
|
||||
}
|
||||
}
|
||||
|
||||
& h2 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
& a:not(:has(img)) {
|
||||
@apply text-[#4493f8] underline underline-offset-2;
|
||||
}
|
||||
|
||||
& h2::before {
|
||||
content: "# ";
|
||||
}
|
||||
|
||||
& h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
& a {
|
||||
@apply no-underline;
|
||||
}
|
||||
}
|
||||
|
||||
& h1,
|
||||
h2 {
|
||||
@apply border-b border-[#5d646da3] pb-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/main.tsx
13
src/main.tsx
@@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.scss";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode } from "react";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { type ReactNode, useState, useEffect } from "react";
|
||||
import { AppContext, type AppContextProps } from "~/context/AppContext";
|
||||
|
||||
export const AppProvider = (props: { children?: ReactNode }) => {
|
||||
const [activeKitty, setActiveKitty] = useState(":r0:");
|
||||
const [brightness, setBrightness] = useState(
|
||||
parseInt(localStorage.getItem("brightness") ?? "100"),
|
||||
);
|
||||
const [volume, setVolume] = useState(
|
||||
parseInt(localStorage.getItem("volume") ?? "100"),
|
||||
);
|
||||
const [state, setState] = useState<AppContextProps["state"]>(
|
||||
(localStorage.getItem("state") as AppContextProps["state"]) ?? "off",
|
||||
);
|
||||
|
||||
const [screenWidth, setScreenWidth] = useState(window.innerWidth);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setScreenWidth(window.innerWidth);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
"state",
|
||||
state === "desktop" ? "login" : state === "boot" ? "off" : state,
|
||||
);
|
||||
localStorage.setItem("brightness", brightness.toString());
|
||||
localStorage.setItem("volume", volume.toString());
|
||||
}, [state, brightness, volume]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
activeKitty,
|
||||
setActiveKitty,
|
||||
brightness,
|
||||
setBrightness,
|
||||
volume,
|
||||
setVolume,
|
||||
screenWidth,
|
||||
state,
|
||||
setState,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { KittyContext } from "~/context/KittyContext";
|
||||
|
||||
export const KittyProvider = KittyContext.Provider;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { type Child, type Icon } from "./tree";
|
||||
|
||||
export const lerpIcon = (icons: Array<string>, value: number, max: number) =>
|
||||
icons[Math.floor((value / max) * icons.length)] ?? icons[icons.length - 1];
|
||||
|
||||
export const getIcon = (file: Child | string | undefined): Icon => {
|
||||
if (!file) return DEFAULT_ICON;
|
||||
|
||||
if (typeof file === "string") {
|
||||
if (ICONS[file]) return ICONS[file];
|
||||
|
||||
const parts = file.split(".");
|
||||
const iconName = parts[parts.length - 1];
|
||||
|
||||
return ICONS[EXT_TO_LANGUAGE[iconName]] ?? DEFAULT_ICON;
|
||||
}
|
||||
|
||||
return file.icon ?? DEFAULT_ICON;
|
||||
};
|
||||
|
||||
export const EXT_TO_LANGUAGE: Record<string, string> = {
|
||||
asc: "Key",
|
||||
md: "Markdown",
|
||||
};
|
||||
|
||||
export const ICONS: Record<string, Icon> = {
|
||||
Markdown: {
|
||||
char: " ",
|
||||
color: "#89bafa",
|
||||
},
|
||||
Key: {
|
||||
char: " ",
|
||||
color: "#f9e2af",
|
||||
},
|
||||
TypeScript: {
|
||||
char: " ",
|
||||
color: "#4d86a2",
|
||||
},
|
||||
Rust: {
|
||||
char: " ",
|
||||
color: "#be8f78",
|
||||
},
|
||||
Instagram: {
|
||||
char: " ",
|
||||
color: "#e1306c",
|
||||
},
|
||||
Github: {
|
||||
char: " ",
|
||||
color: "#ffffff",
|
||||
},
|
||||
LinkedIn: {
|
||||
char: " ",
|
||||
color: "#0077b5",
|
||||
},
|
||||
CodinGame: {
|
||||
char: " ",
|
||||
color: "#f1c40f",
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_ICON = { char: " ", color: "#f599ae" };
|
||||
@@ -1,5 +0,0 @@
|
||||
export const clamp = (v: number, min: number, max: number) =>
|
||||
Math.min(Math.max(v, min), max);
|
||||
|
||||
export const randomMinMax = (min: number, max: number) =>
|
||||
Math.floor(Math.random() * (max - min) + min);
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type ClassNameValue, twMerge } from "tailwind-merge";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
export const cn = (...classes: Array<ClassNameValue>) => twMerge(clsx(classes));
|
||||
|
||||
export const hideIf = (condition: boolean) => (condition ? "hidden" : "");
|
||||
@@ -1,26 +0,0 @@
|
||||
export class CharArray {
|
||||
private readonly chars: string[];
|
||||
|
||||
constructor(fill: string, size: number) {
|
||||
this.chars = fill.repeat(size).split("");
|
||||
}
|
||||
|
||||
set(i: number, char: string | undefined) {
|
||||
if (char === undefined || i < 0 || i >= this.chars.length) return;
|
||||
this.chars[i] = char;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
write(i: number, str: string) {
|
||||
for (let oi = 0; oi < str.length; oi++) {
|
||||
this.set(i + oi, str[oi]);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.chars.join("");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export const formatMMSS = (time: number) => {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
|
||||
const minutesString = minutes.toString().padStart(2, "0");
|
||||
const secondsString = seconds.toString().padStart(2, "0");
|
||||
|
||||
return `${minutesString}:${secondsString}`;
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import { DEFAULT_ICON, ICONS, getIcon } from "./icons";
|
||||
|
||||
export type Icon = {
|
||||
char: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
type: "project";
|
||||
name: string;
|
||||
url: string;
|
||||
private: boolean;
|
||||
content: string;
|
||||
icon: Icon;
|
||||
};
|
||||
|
||||
export type File = {
|
||||
type: "file";
|
||||
name: string;
|
||||
content: string;
|
||||
icon: Icon;
|
||||
};
|
||||
|
||||
export type Link = {
|
||||
type: "link";
|
||||
name: string;
|
||||
url: string;
|
||||
icon: Icon;
|
||||
};
|
||||
|
||||
export type Folder = {
|
||||
name: string;
|
||||
type: "folder";
|
||||
children: Array<Child>;
|
||||
opened: boolean;
|
||||
};
|
||||
|
||||
export type Child = Link | Project | File;
|
||||
|
||||
export const sortFiles = (files: Array<Folder | Child>) =>
|
||||
files
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) =>
|
||||
a.type === "folder" && b.type !== "folder"
|
||||
? -1
|
||||
: a.type !== "folder" && b.type === "folder"
|
||||
? 1
|
||||
: 0,
|
||||
);
|
||||
|
||||
export const folder = (name: string, children: Array<Child>): Folder => ({
|
||||
type: "folder",
|
||||
name,
|
||||
opened: false,
|
||||
children,
|
||||
});
|
||||
|
||||
export const link = (name: string, url: string, icon: string): Link => ({
|
||||
type: "link",
|
||||
name,
|
||||
url,
|
||||
icon: getIcon(icon),
|
||||
});
|
||||
|
||||
export const file = (
|
||||
name: string,
|
||||
content: string,
|
||||
icon: Icon = getIcon(name),
|
||||
): File => ({
|
||||
type: "file",
|
||||
name,
|
||||
content,
|
||||
icon,
|
||||
});
|
||||
|
||||
export const project = (
|
||||
name: string,
|
||||
content: string,
|
||||
url: string,
|
||||
language: string,
|
||||
priv: boolean,
|
||||
): Project => ({
|
||||
type: "project",
|
||||
name,
|
||||
content,
|
||||
url,
|
||||
icon: ICONS[language] ?? DEFAULT_ICON,
|
||||
private: priv,
|
||||
});
|
||||
|
||||
export const icon = (char: string, color: string): Icon => ({
|
||||
char,
|
||||
color,
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { type KittyContextProps } from "~/context/KittyContext";
|
||||
|
||||
export type Prettify<T> = NonNullable<{ [K in keyof T]: T[K] }>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type InnerKittyProps<T extends (...args: any[]) => any> = Prettify<
|
||||
Parameters<T>[0] & KittyContextProps
|
||||
>;
|
||||
|
||||
export type State<
|
||||
Name extends string,
|
||||
T,
|
||||
> = Name extends `${infer First}${infer Rest}`
|
||||
? {
|
||||
[K in Name]: T;
|
||||
} & { [K in `set${Capitalize<First>}${Rest}`]: (value: T) => void }
|
||||
: never;
|
||||
@@ -1,77 +0,0 @@
|
||||
import { type Config } from "tailwindcss";
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
|
||||
const config = {
|
||||
content: ["index.html", "./src/**/*.tsx", "./src/**/*.ts"],
|
||||
theme: {
|
||||
extend: {
|
||||
keyframes: {
|
||||
fadeOut: {
|
||||
"0%": { opacity: "1" },
|
||||
"100%": { opacity: "0" },
|
||||
},
|
||||
breathing: {
|
||||
"0%, 100%": { transform: "scale(1)", opacity: "0.9" },
|
||||
"50%": {
|
||||
transform: "scale(1.1)",
|
||||
opacity: "1",
|
||||
},
|
||||
},
|
||||
disappear: {
|
||||
"0%": { transform: "scale(1)", opacity: "0.95" },
|
||||
"20%": { transform: "scale(1.2)", opacity: "1" },
|
||||
"100%": { transform: "scale(0)", opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
fadeOut: "fadeOut 250ms ease-out forwards",
|
||||
breathing: "breathing 4s ease-in-out infinite",
|
||||
disappear: "disappear 750ms ease-in-out forwards",
|
||||
},
|
||||
fontSize: {
|
||||
sm: "0.8rem",
|
||||
base: "1rem",
|
||||
lg: "1.25rem",
|
||||
xl: "1.25rem",
|
||||
"2xl": "1.563rem",
|
||||
"3xl": "1.953rem",
|
||||
"4xl": "2.441rem",
|
||||
"5xl": "3.052rem",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
body: ["JetBrainsMono", "mono"],
|
||||
},
|
||||
boxShadow: {
|
||||
window: "0 0 1px 1px #1a1a1a",
|
||||
},
|
||||
colors: {
|
||||
foreground: "#cdd6f4",
|
||||
background: "#1e1e2e",
|
||||
borderInactive: "#595959",
|
||||
borderActive: "#cdd6f4",
|
||||
selectionForeground: "#1e1e2e",
|
||||
selectionBackground: "#f5e0dc",
|
||||
color0: "#45475a",
|
||||
color1: "#f38ba8",
|
||||
color2: "#a6e3a1",
|
||||
color3: "#f9e2af",
|
||||
color4: "#89bafa",
|
||||
color5: "#f5c2e7",
|
||||
color6: "#94e2d5",
|
||||
color7: "#bac2de",
|
||||
color8: "#585B70",
|
||||
color9: "#f38ba8",
|
||||
color10: "#a6e3a1",
|
||||
color11: "#f9e2af",
|
||||
color12: "#89bafa",
|
||||
color13: "#f5c2e7",
|
||||
color14: "#94e2d5",
|
||||
color15: "#a6adc8",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} as const satisfies Config;
|
||||
|
||||
export default config;
|
||||
2
todo.txt
2
todo.txt
@@ -1,2 +0,0 @@
|
||||
- change title based on focused kitty
|
||||
- fetch currently playing song
|
||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,30 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["build", "vite.config.ts", "tailwind.config.ts"]
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { manifest } from "./build/manifestPlugin";
|
||||
|
||||
const config = defineConfig({
|
||||
plugins: [react(), manifest(), tsconfigPaths()],
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user