diff --git a/components.json b/components.json new file mode 100644 index 0000000..29bb0c9 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "", + "css": "src/style.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/index.html b/index.html index 438e541..b9eb454 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - tobeplayed-frontend + Game Log
diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..abe04df --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/package-lock.json b/package-lock.json index 313b379..e723193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,22 @@ "name": "tobeplayed-frontend", "version": "0.0.1", "dependencies": { + "@fontsource-variable/inter": "^5.3.0", "@hookform/resolvers": "^3.10.0", - "@mdi/font": "~7.4.47", "@tailwindcss/vite": "~4.3.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "i18next": "~26.4.2", "i18next-http-backend": "~3.0.6", + "lucide-react": "^1.48.0", + "radix-ui": "^1.6.7", "react": "^19.3.0", "react-dom": "^19.3.0", "react-hook-form": "^7.88.0", "react-i18next": "~17.0.15", "react-router-dom": "~7.18.4", + "sonner": "^2.0.8", + "tailwind-merge": "^3.7.0", "zod": "^3.25.76" }, "devDependencies": { @@ -25,6 +31,7 @@ "autoprefixer": "~10.6.1", "postcss": "~8.5.28", "tailwindcss": "~4.3.3", + "tw-animate-css": "^1.4.0", "vite": "^8.3.0", "vitest": "^5.0.1" } @@ -69,6 +76,53 @@ "tslib": "^2.4.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@hookform/resolvers": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", @@ -123,12 +177,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mdi/font": { - "version": "7.4.47", - "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", - "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==", - "license": "Apache-2.0" - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -156,6 +204,1502 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.15.tgz", + "integrity": "sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", + "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.23.tgz", + "integrity": "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.15.tgz", + "integrity": "sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.7.tgz", + "integrity": "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.16.tgz", + "integrity": "sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.23.tgz", + "integrity": "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.24.tgz", + "integrity": "sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.22.tgz", + "integrity": "sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.16.tgz", + "integrity": "sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.11.tgz", + "integrity": "sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.7.tgz", + "integrity": "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.7.tgz", + "integrity": "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.23.tgz", + "integrity": "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.19.tgz", + "integrity": "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-toggle-group": "1.1.19" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.5.tgz", + "integrity": "sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.10.tgz", @@ -772,6 +2316,18 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -897,6 +2453,27 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -928,6 +2505,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.438", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.438.tgz", @@ -1030,6 +2613,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1340,6 +2932,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lucide-react": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.48.0.tgz", + "integrity": "sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1464,6 +3065,83 @@ "dev": true, "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.7.tgz", + "integrity": "sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-accessible-icon": "1.1.15", + "@radix-ui/react-accordion": "1.2.20", + "@radix-ui/react-alert-dialog": "1.1.23", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-aspect-ratio": "1.1.15", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-checkbox": "1.3.11", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-context-menu": "2.3.7", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-form": "0.1.16", + "@radix-ui/react-hover-card": "1.1.23", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-menubar": "1.1.24", + "@radix-ui/react-navigation-menu": "1.2.22", + "@radix-ui/react-one-time-password-field": "0.1.16", + "@radix-ui/react-password-toggle-field": "0.1.11", + "@radix-ui/react-popover": "1.1.23", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-progress": "1.1.16", + "@radix-ui/react-radio-group": "1.4.7", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-scroll-area": "1.2.18", + "@radix-ui/react-select": "2.3.7", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slider": "1.4.7", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-switch": "1.3.7", + "@radix-ui/react-tabs": "1.1.21", + "@radix-ui/react-toast": "1.2.23", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-toggle-group": "1.1.19", + "@radix-ui/react-toolbar": "1.1.19", + "@radix-ui/react-tooltip": "1.2.16", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-escape-keydown": "1.1.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", @@ -1528,6 +3206,53 @@ } } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.18.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", @@ -1566,6 +3291,28 @@ "react-dom": ">=18" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/rolldown": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.10.tgz", @@ -1618,6 +3365,22 @@ "dev": true, "license": "ISC" }, + "node_modules/sonner": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz", + "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1641,6 +3404,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -1706,8 +3479,17 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } }, "node_modules/update-browserslist-db": { "version": "1.3.3", @@ -1740,6 +3522,49 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/package.json b/package.json index b340074..f908f11 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,22 @@ "test": "vitest run" }, "dependencies": { + "@fontsource-variable/inter": "^5.3.0", "@hookform/resolvers": "^3.10.0", - "@mdi/font": "~7.4.47", "@tailwindcss/vite": "~4.3.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "i18next": "~26.4.2", "i18next-http-backend": "~3.0.6", + "lucide-react": "^1.48.0", + "radix-ui": "^1.6.7", "react": "^19.3.0", "react-dom": "^19.3.0", "react-hook-form": "^7.88.0", "react-i18next": "~17.0.15", "react-router-dom": "~7.18.4", + "sonner": "^2.0.8", + "tailwind-merge": "^3.7.0", "zod": "^3.25.76" }, "devDependencies": { @@ -27,6 +33,7 @@ "autoprefixer": "~10.6.1", "postcss": "~8.5.28", "tailwindcss": "~4.3.3", + "tw-animate-css": "^1.4.0", "vite": "^8.3.0", "vitest": "^5.0.1" } diff --git a/public/i18n/de.json b/public/i18n/de.json index 8dc7a6a..9d2402e 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -32,7 +32,7 @@ "TITLE": "Einstellungen", "LOADING": "Einstellungen werden geladen …", "ACCOUNT": "Konto", - "PREFERENCES": "Einstellungen", + "PREFERENCES": "Darstellung & Sprache", "STEAM": "Steam", "EMAIL": "Email", "ROLE": "Rolle", @@ -72,7 +72,9 @@ "STEAM_GAMES": "Steam Spiele", "NO_STEAM_ID": "Keine Steam-ID hinterlegt – jetzt in den Einstellungen eintragen", "MOST_PLAYED": "Meistgespielt", - "SHOW_ALL": "Alle anzeigen" + "SHOW_ALL": "Alle anzeigen", + "TO_LIBRARY": "Zur Bibliothek", + "WELCOME_BACK": "Willkommen zurück" }, "LIBRARY": { "TITLE": "Spielebibliothek", @@ -138,6 +140,10 @@ "DASHBOARD": "Dashboard", "LIBRARY": "Bibliothek", "SETTINGS": "Einstellungen", - "TO_DASHBOARD": "Zum Dashboard" + "TO_DASHBOARD": "Zum Dashboard", + "MENU": "Menü", + "LANGUAGE": "Sprache", + "DARK_MODE": "Dunkles Design", + "LIGHT_MODE": "Helles Design" } } diff --git a/public/i18n/en.json b/public/i18n/en.json index 673a9d9..d5d2790 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -72,7 +72,9 @@ "STEAM_GAMES": "Steam Games", "NO_STEAM_ID": "No Steam ID saved yet – add it in the settings", "MOST_PLAYED": "Most played", - "SHOW_ALL": "Show all" + "SHOW_ALL": "Show all", + "TO_LIBRARY": "Go to library", + "WELCOME_BACK": "Welcome back" }, "LIBRARY": { "TITLE": "Game Library", @@ -138,6 +140,10 @@ "DASHBOARD": "Dashboard", "LIBRARY": "Library", "SETTINGS": "Settings", - "TO_DASHBOARD": "Go to dashboard" + "TO_DASHBOARD": "Go to dashboard", + "MENU": "Menu", + "LANGUAGE": "Language", + "DARK_MODE": "Dark mode", + "LIGHT_MODE": "Light mode" } } diff --git a/src/App.jsx b/src/App.jsx index 45d11b2..3a66e2d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import {Link, NavLink, Route, Routes, useLocation, useNavigate} from 'react-router-dom'; +import {Link, Route, Routes, useLocation, useNavigate} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {useEffect, useState} from 'react'; @@ -10,17 +10,13 @@ import RegistrationPage from './pages/RegistrationPage.jsx'; import SettingsPage from './pages/SettingsPage.jsx'; import GameLibrary from './pages/GameLibrary.jsx'; import GameDetail from './pages/GameDetail.jsx'; -import {usePreferences} from './context/PreferencesContext.jsx'; +import AppHeader from './components/AppHeader.jsx'; +import {Toaster} from '@/components/ui/sonner'; +import {TooltipProvider} from '@/components/ui/tooltip'; import {getValidToken} from './utils/auth.js'; -const navLinkClass = ({isActive}) => - `flex items-center gap-1 text-sm font-bold transition-colors ${ - isActive ? 'text-blue-600 dark:text-blue-400' : 'text-slate-600 dark:text-slate-300 hover:text-blue-500' - }`; - function App() { - const {t, i18n} = useTranslation(); - const {isDarkMode, setDarkMode, setLanguage} = usePreferences(); + const {t} = useTranslation(); const [isAuthenticated, setIsAuthenticated] = useState(false); // Hooks for routing @@ -31,10 +27,6 @@ function App() { setIsAuthenticated(!!getValidToken()); }, [location]); - const toggleLanguage = () => { - setLanguage(i18n.language === 'de' ? 'en' : 'de'); - }; - const handleLogout = () => { localStorage.removeItem('tbp_jwt'); setIsAuthenticated(false); @@ -42,73 +34,32 @@ function App() { }; return ( -
-
- - Game Log - + +
+ - {isAuthenticated && ( - - )} +
+ + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + +
-
- - - {isAuthenticated ? ( - <> - - - ) : ( - - {t('MAIN.LOGIN')} - - )} -
-
- -
- - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - -
- - -
+ + + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/components/AppHeader.jsx b/src/components/AppHeader.jsx new file mode 100644 index 0000000..db78e25 --- /dev/null +++ b/src/components/AppHeader.jsx @@ -0,0 +1,98 @@ +import {Link, NavLink} from 'react-router-dom'; +import {useTranslation} from 'react-i18next'; +import {Gamepad2, Languages, LayoutDashboard, Library, LogOut, Moon, Settings, Sun, UserRound} from 'lucide-react'; + +import {Button} from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import {usePreferences} from '@/context/PreferencesContext.jsx'; +import {cn} from '@/lib/utils'; + +const NAV_ITEMS = [ + {to: '/dashboard', labelKey: 'NAV.DASHBOARD', Icon: LayoutDashboard}, + {to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library}, +]; + +const navLinkClass = ({isActive}) => cn( + 'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors', + isActive + ? 'bg-accent text-accent-foreground' + : 'text-muted-foreground hover:bg-muted hover:text-foreground', +); + +export default function AppHeader({isAuthenticated, onLogout}) { + const {t, i18n} = useTranslation(); + const {isDarkMode, setDarkMode, setLanguage} = usePreferences(); + + const toggleLanguage = () => setLanguage(i18n.language === 'de' ? 'en' : 'de'); + const toggleTheme = () => setDarkMode(!isDarkMode); + const ThemeIcon = isDarkMode ? Sun : Moon; + + return ( +
+
+ + + + + Game Log + + + {isAuthenticated && ( + + )} + +
+ {isAuthenticated ? ( + + + + + + + {t('NAV.SETTINGS')} + + + {t('NAV.LANGUAGE')}: {i18n.language.toUpperCase()} + + + {t(isDarkMode ? 'NAV.LIGHT_MODE' : 'NAV.DARK_MODE')} + + + + {t('MAIN.LOGOUT')} + + + + ) : ( + <> + + + + + )} +
+
+
+ ); +} diff --git a/src/components/AuthLayout.jsx b/src/components/AuthLayout.jsx new file mode 100644 index 0000000..64852bc --- /dev/null +++ b/src/components/AuthLayout.jsx @@ -0,0 +1,27 @@ +import {Gamepad2} from 'lucide-react'; + +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; + +/** + * Centered card for the login and registration forms. + */ +export default function AuthLayout({title, description, children, footer}) { + return ( +
+
+ + + + + + {title} + {description && {description}} + + + {children} + {footer &&

{footer}

} +
+
+
+ ); +} diff --git a/src/components/FeedbackAlert.jsx b/src/components/FeedbackAlert.jsx new file mode 100644 index 0000000..82d2dc6 --- /dev/null +++ b/src/components/FeedbackAlert.jsx @@ -0,0 +1,14 @@ +import {CircleAlert, CircleCheck} from 'lucide-react'; + +import {Alert, AlertDescription} from '@/components/ui/alert'; + +export default function FeedbackAlert({message, isError}) { + if (!message) return null; + const Icon = isError ? CircleAlert : CircleCheck; + return ( + + + {message} + + ); +} diff --git a/src/components/FormField.jsx b/src/components/FormField.jsx new file mode 100644 index 0000000..e5e4cb0 --- /dev/null +++ b/src/components/FormField.jsx @@ -0,0 +1,15 @@ +import {Label} from '@/components/ui/label'; + +/** + * Label, input and the translated validation error of one form field. + * `error` is a react-hook-form field error whose message is an i18n key. + */ +export default function FormField({id, label, error, t, children}) { + return ( +
+ + {children} + {error &&

{t(error.message)}

} +
+ ); +} diff --git a/src/components/GameCover.jsx b/src/components/GameCover.jsx new file mode 100644 index 0000000..ebf658c --- /dev/null +++ b/src/components/GameCover.jsx @@ -0,0 +1,30 @@ +import {useState} from 'react'; +import {Gamepad2} from 'lucide-react'; + +import {cn} from '@/lib/utils'; +import {coverUrl} from '@/utils/games.js'; + +/** + * Steam store header (460x215). Falls back to a neutral tile when the app has no header image. + */ +export default function GameCover({appid, className}) { + const [failed, setFailed] = useState(false); + + return ( +
+ {failed ? ( +
+ +
+ ) : ( + setFailed(true)} + /> + )} +
+ ); +} diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx index 2034329..9599219 100644 --- a/src/components/StatCard.jsx +++ b/src/components/StatCard.jsx @@ -1,4 +1,7 @@ import {useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; + +import {Card} from '@/components/ui/card'; function useCountUp(target, durationMs = 1600) { const [value, setValue] = useState(0); @@ -22,19 +25,20 @@ function useCountUp(target, durationMs = 1600) { return value; } -export default function StatCard({label, value, icon, suffix}) { +export default function StatCard({label, value, icon: Icon, suffix}) { + const {i18n} = useTranslation(); const animated = useCountUp(value); return ( -
- {icon && ( - + + {Icon && ( + + + )} -
- {animated.toLocaleString()}{suffix ?? ''} +
+ {animated.toLocaleString(i18n.language)}{suffix ?? ''}
-
- {label} -
-
+
{label}
+
); } diff --git a/src/components/SteamGameCount.jsx b/src/components/SteamGameCount.jsx index 1b7ec65..dbee51d 100644 --- a/src/components/SteamGameCount.jsx +++ b/src/components/SteamGameCount.jsx @@ -1,43 +1,45 @@ -import React, {useState, useEffect} from 'react'; +import {useEffect, useState} from 'react'; import {Link} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; +import {ArrowRight, Gamepad2} from 'lucide-react'; + +import {Card} from '@/components/ui/card'; + +const DURATION_MS = 1000; const SteamGameCount = ({targetCount}) => { const {t} = useTranslation(); const [count, setCount] = useState(0); useEffect(() => { + let frame; let startTime; - const duration = 1000; - const startCount = 0; - const animate = (timestamp) => { - if (!startTime) startTime = timestamp; - const progress = Math.min((timestamp - startTime) / duration, 1); - - const currentCount = Math.floor(progress * (targetCount - startCount) + startCount); - setCount(currentCount); - + startTime ??= timestamp; + const progress = Math.min((timestamp - startTime) / DURATION_MS, 1); + setCount(Math.floor(progress * targetCount)); if (progress < 1) { - requestAnimationFrame(animate); + frame = requestAnimationFrame(animate); } }; - - requestAnimationFrame(animate); + frame = requestAnimationFrame(animate); + return () => cancelAnimationFrame(frame); }, [targetCount]); return ( - -
- {count} + +
+
+
-
- {t('DASHBOARD.STEAM_GAMES')} +
+
{count}
+
{t('DASHBOARD.STEAM_GAMES')}
- + + {t('DASHBOARD.TO_LIBRARY')} + + ); }; diff --git a/src/components/SyncStatusBadge.jsx b/src/components/SyncStatusBadge.jsx index 52dca05..793cca5 100644 --- a/src/components/SyncStatusBadge.jsx +++ b/src/components/SyncStatusBadge.jsx @@ -1,4 +1,5 @@ import {useTranslation} from 'react-i18next'; +import {CircleAlert, CircleCheck, RefreshCw} from 'lucide-react'; const formatRelative = (iso, locale) => { if (!iso) return null; @@ -16,8 +17,8 @@ export default function SyncStatusBadge({status, lastSyncedAt, isRunning}) { if (isRunning) { return ( -
- +
+ {t('LIBRARY.SYNCING')}
); @@ -25,8 +26,8 @@ export default function SyncStatusBadge({status, lastSyncedAt, isRunning}) { if (status === 'FAILED') { return ( -
- +
+ {t('LIBRARY.SYNC_FAILED')}
); @@ -34,8 +35,8 @@ export default function SyncStatusBadge({status, lastSyncedAt, isRunning}) { if (lastSyncedAt) { return ( -
- +
+ {t('LIBRARY.LAST_SYNCED', {when: formatRelative(lastSyncedAt, i18n.language)})}
); diff --git a/src/components/ui/alert.jsx b/src/components/ui/alert.jsx new file mode 100644 index 0000000..607c06c --- /dev/null +++ b/src/components/ui/alert.jsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { cva } from "class-variance-authority"; +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}) { + return ( +
+ ) +} + +function AlertTitle({ + className, + ...props +}) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/badge.jsx b/src/components/ui/badge.jsx new file mode 100644 index 0000000..878ca5a --- /dev/null +++ b/src/components/ui/badge.jsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { cva } from "class-variance-authority"; +import { cn } from "@/lib/utils" +import { Slot } from "radix-ui" + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx new file mode 100644 index 0000000..e744b1c --- /dev/null +++ b/src/components/ui/button.jsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { cva } from "class-variance-authority"; +import { cn } from "@/lib/utils" +import { Slot } from "radix-ui" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/card.jsx b/src/components/ui/card.jsx new file mode 100644 index 0000000..526c1b5 --- /dev/null +++ b/src/components/ui/card.jsx @@ -0,0 +1,112 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +function Card({ + className, + ...props +}) { + return ( +
+ ) +} + +function CardHeader({ + className, + ...props +}) { + return ( +
+ ) +} + +function CardTitle({ + className, + ...props +}) { + return ( +
+ ) +} + +function CardDescription({ + className, + ...props +}) { + return ( +
+ ) +} + +function CardAction({ + className, + ...props +}) { + return ( +
+ ) +} + +function CardContent({ + className, + ...props +}) { + return ( +
+ ) +} + +function CardFooter({ + className, + ...props +}) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/dropdown-menu.jsx b/src/components/ui/dropdown-menu.jsx new file mode 100644 index 0000000..daacdf6 --- /dev/null +++ b/src/components/ui/dropdown-menu.jsx @@ -0,0 +1,249 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" + +function DropdownMenu({ + ...props +}) { + return +} + +function DropdownMenuPortal({ + ...props +}) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/src/components/ui/input.jsx b/src/components/ui/input.jsx new file mode 100644 index 0000000..292ed7d --- /dev/null +++ b/src/components/ui/input.jsx @@ -0,0 +1,24 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +function Input({ + className, + type, + ...props +}) { + return ( + + ) +} + +export { Input } diff --git a/src/components/ui/label.jsx b/src/components/ui/label.jsx new file mode 100644 index 0000000..e531796 --- /dev/null +++ b/src/components/ui/label.jsx @@ -0,0 +1,21 @@ +import * as React from "react" +import { cn } from "@/lib/utils" +import { Label as LabelPrimitive } from "radix-ui" + +function Label({ + className, + ...props +}) { + return ( + + ) +} + +export { Label } diff --git a/src/components/ui/select.jsx b/src/components/ui/select.jsx new file mode 100644 index 0000000..e0a5d56 --- /dev/null +++ b/src/components/ui/select.jsx @@ -0,0 +1,187 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { Select as SelectPrimitive } from "radix-ui" + +function Select({ + ...props +}) { + return +} + +function SelectGroup({ + ...props +}) { + return +} + +function SelectValue({ + ...props +}) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/src/components/ui/separator.jsx b/src/components/ui/separator.jsx new file mode 100644 index 0000000..7effbfc --- /dev/null +++ b/src/components/ui/separator.jsx @@ -0,0 +1,25 @@ +import * as React from "react" +import { cn } from "@/lib/utils" +import { Separator as SeparatorPrimitive } from "radix-ui" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}) { + return ( + + ) +} + +export { Separator } diff --git a/src/components/ui/skeleton.jsx b/src/components/ui/skeleton.jsx new file mode 100644 index 0000000..144a97a --- /dev/null +++ b/src/components/ui/skeleton.jsx @@ -0,0 +1,16 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/src/components/ui/sonner.jsx b/src/components/ui/sonner.jsx new file mode 100644 index 0000000..aaa89c6 --- /dev/null +++ b/src/components/ui/sonner.jsx @@ -0,0 +1,43 @@ +"use client" + +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { usePreferences } from "@/context/PreferencesContext.jsx" +import { Toaster as Sonner } from "sonner"; + +const Toaster = ({ + ...props +}) => { + const { isDarkMode } = usePreferences() + const theme = isDarkMode ? "dark" : "light" + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)" + } + } + {...props} + /> + ); +} + +export { Toaster } diff --git a/src/components/ui/switch.jsx b/src/components/ui/switch.jsx new file mode 100644 index 0000000..67f4af2 --- /dev/null +++ b/src/components/ui/switch.jsx @@ -0,0 +1,30 @@ +import * as React from "react" +import { cn } from "@/lib/utils" +import { Switch as SwitchPrimitive } from "radix-ui" + +function Switch({ + className, + size = "default", + ...props +}) { + return ( + + + + ) +} + +export { Switch } diff --git a/src/components/ui/tooltip.jsx b/src/components/ui/tooltip.jsx new file mode 100644 index 0000000..5d82fd5 --- /dev/null +++ b/src/components/ui/tooltip.jsx @@ -0,0 +1,54 @@ +import * as React from "react" +import { cn } from "@/lib/utils" +import { Tooltip as TooltipPrimitive } from "radix-ui" + +function TooltipProvider({ + delayDuration = 0, + ...props +}) { + return ( + + ) +} + +function Tooltip({ + ...props +}) { + return +} + +function TooltipTrigger({ + ...props +}) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/hooks/useForceSync.js b/src/hooks/useForceSync.js index 8b7fdd5..6add492 100644 --- a/src/hooks/useForceSync.js +++ b/src/hooks/useForceSync.js @@ -1,18 +1,18 @@ import {useState} from 'react'; import {useNavigate} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; +import {toast} from 'sonner'; const RELOAD_DELAY_MS = 4000; /** - * Starts a library sync (POST /api/library/sync) and calls onStarted after a short delay, - * so the caller can reload once Steam data has arrived. + * Starts a library sync (POST /api/library/sync), reports the result as a toast and calls + * onStarted after a short delay, so the caller can reload once Steam data has arrived. */ export default function useForceSync(onStarted) { const {t} = useTranslation(); const navigate = useNavigate(); const [busy, setBusy] = useState(false); - const [message, setMessage] = useState(null); const trigger = async () => { const token = localStorage.getItem('tbp_jwt'); @@ -21,7 +21,6 @@ export default function useForceSync(onStarted) { return; } setBusy(true); - setMessage(null); try { const response = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/sync`, { method: 'POST', @@ -29,22 +28,22 @@ export default function useForceSync(onStarted) { }); if (response.status === 429) { const json = await response.json().catch(() => ({})); - setMessage(t('LIBRARY.SYNC_COOLDOWN', {seconds: json.retryAfterSeconds ?? 60})); + toast.warning(t('LIBRARY.SYNC_COOLDOWN', {seconds: json.retryAfterSeconds ?? 60})); return; } if (!response.ok) { - setMessage(t('LIBRARY.SYNC_FAILED')); + toast.error(t('LIBRARY.SYNC_FAILED')); return; } - setMessage(t('LIBRARY.SYNC_STARTED')); + toast.success(t('LIBRARY.SYNC_STARTED')); setTimeout(onStarted, RELOAD_DELAY_MS); } catch (err) { console.error('sync trigger error:', err); - setMessage(t('INFO.ERROR.NETWORK_FAILURE')); + toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } finally { setBusy(false); } }; - return {trigger, busy, message}; + return {trigger, busy}; } diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 0000000..c3c6060 --- /dev/null +++ b/src/lib/utils.js @@ -0,0 +1,6 @@ +import {clsx} from 'clsx'; +import {twMerge} from 'tailwind-merge'; + +export function cn(...inputs) { + return twMerge(clsx(inputs)); +} diff --git a/src/main.jsx b/src/main.jsx index eb86d6b..27c38a3 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -5,7 +5,6 @@ import App from './App.jsx'; import {PreferencesProvider} from './context/PreferencesContext.jsx'; import './i18n'; import './style.css'; -import '@mdi/font/css/materialdesignicons.css'; createRoot(document.getElementById('root')).render( diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 3bb37ea..1274b26 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,18 +1,23 @@ import React, {useCallback, useEffect, useState} from 'react'; import {Link, useNavigate} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; +import {ArrowRight, Hash, Link2, RefreshCw, Trophy} from 'lucide-react'; + +import {Badge} from '@/components/ui/badge'; +import {Button} from '@/components/ui/button'; +import {Card} from '@/components/ui/card'; +import {Skeleton} from '@/components/ui/skeleton'; +import GameCover from '../components/GameCover.jsx'; import SteamGameCount from '../components/SteamGameCount'; import SyncStatusBadge from '../components/SyncStatusBadge.jsx'; import useLibrarySync from '../hooks/useLibrarySync.js'; import useForceSync from '../hooks/useForceSync.js'; -import {formatPlaytime, iconUrl} from '../utils/games.js'; +import {formatPlaytime} from '../utils/games.js'; const TOP_GAMES = 5; // The games endpoint accepts only the page sizes 20, 40 and 80. const TOP_GAMES_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc'; -const cardClass = 'bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700'; - const authHeaders = () => ({ 'Authorization': `Bearer ${localStorage.getItem('tbp_jwt')}`, 'Content-Type': 'application/json', @@ -101,120 +106,123 @@ const Dashboard = () => { const forceSync = useForceSync(reloadLibrary); if (isLoading) { - return ( -
- {t('DASHBOARD.LOADING')} -
- ); + return ; } if (error) { return ( -
+
{error}
); } const syncBusy = forceSync.busy || sync.isRunning; + const maxMinutes = topGames[0]?.playtimeMinutes || 1; return (
-
-
-

+ +
+
+

{t('DASHBOARD.WELCOME_BACK')}

+

{t('DASHBOARD.HELLO')} {userData?.email?.split('@')[0]} 👋

{hasSteamId ? ( -

- - {t('SETTINGS.STEAM_ID')}: {userData.steamId} -

+ + + {userData.steamId} + ) : ( - - - {t('DASHBOARD.NO_STEAM_ID')} - + )}
{hasSteamId && ( -
+
- - {forceSync.message && ( - {forceSync.message} - )} +
)} -

+ {hasSteamId && (
-
-
-

- + +
+

+ {t('DASHBOARD.MOST_PLAYED')}

- - {t('DASHBOARD.SHOW_ALL')} - +
{topGames.length === 0 ? ( -

+

{sync.isRunning ? t('LIBRARY.SYNCING_FIRST_TIME') : t('LIBRARY.EMPTY')}

) : ( -
    - {topGames.map((g, index) => { - const icon = iconUrl(g.appid, g.imgIconUrl); - return ( -
  1. - - - {index + 1}. - - {icon ? ( - - ) : ( -
    - )} - +
      + {topGames.map((g, index) => ( +
    1. + + + {index + 1} + + +
      +
      {g.name ?? `App ${g.appid}`} - - - {formatPlaytime(g.playtimeMinutes)} - - -
    2. - ); - })} +
    +
    +
    +
    +
    + + {formatPlaytime(g.playtimeMinutes)} + + +
  2. + ))}
)} -

+
)}
); }; +function DashboardSkeleton() { + return ( +
+ +
+ + +
+
+ ); +} + export default Dashboard; diff --git a/src/pages/GameDetail.jsx b/src/pages/GameDetail.jsx index 8fb6601..2463d36 100644 --- a/src/pages/GameDetail.jsx +++ b/src/pages/GameDetail.jsx @@ -1,7 +1,15 @@ import {useEffect, useMemo, useState} from 'react'; import {Link, useNavigate, useParams} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; -import {formatPlaytime, iconUrl} from '../utils/games.js'; +import {ArrowLeft, Clock, ExternalLink, ShieldOff, Trophy} from 'lucide-react'; + +import {Badge} from '@/components/ui/badge'; +import {Button} from '@/components/ui/button'; +import {Card} from '@/components/ui/card'; +import {Skeleton} from '@/components/ui/skeleton'; +import {cn} from '@/lib/utils'; +import GameCover from '../components/GameCover.jsx'; +import {formatPlaytime} from '../utils/games.js'; const formatUnlockedAt = (instant, locale) => { if (!instant) return ''; @@ -80,169 +88,129 @@ export default function GameDetail() { }); }, [data]); + const stats = data ? [ + [t('GAME_DETAIL.UNLOCKED'), data.achievementsUnlocked], + [t('GAME_DETAIL.REMAINING'), data.achievementsRemaining], + [t('GAME_DETAIL.COMPLETION'), `${data.completionPercent}%`], + ] : []; + return ( -
-
- +
+ - {isLoading && ( -
- {t('GAME_DETAIL.LOADING')} -
- )} + {isLoading && ( +
+ + +
+ )} - {error && !isLoading && ( -
- {error} -
- - {t('GAME_DETAIL.BACK_TO_LIBRARY')} - -
-
- )} + {error && !isLoading && ( +
+

{error}

+ +
+ )} - {!isLoading && !error && data && ( - <> -
- {iconUrl(data.appid, data.imgIconUrl) ? ( - - ) : ( -
- )} -
-

+ {!isLoading && !error && data && ( + <> + + +
+
+

{data.name ?? `App ${data.appid}`}

-

+ + {t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(data.playtimeMinutes)})} -

+
- - {t('GAME_DETAIL.OPEN_IN_STEAM')} - -

+ +
+ - {!data.hasAchievements && ( -
- {t('GAME_DETAIL.NO_ACHIEVEMENTS')} -
- )} + {!data.hasAchievements && ( + + + {t('GAME_DETAIL.NO_ACHIEVEMENTS')} + + )} - {data.hasAchievements && !data.statsVisible && ( -
- - {t('GAME_DETAIL.STATS_HIDDEN')} -
- )} + {data.hasAchievements && !data.statsVisible && ( + + + {t('GAME_DETAIL.STATS_HIDDEN')} + + )} - {data.hasAchievements && data.statsVisible && ( - <> -
-
-

- {t('GAME_DETAIL.ACHIEVEMENTS')} -

- - {data.achievementsUnlocked} / {data.achievementsTotal} - {' · '} - {data.completionPercent}% - -
-
-
-
-
-
-
- {data.achievementsUnlocked} -
-
- {t('GAME_DETAIL.UNLOCKED')} -
+ {data.hasAchievements && data.statsVisible && ( + <> + +
+

+ + {t('GAME_DETAIL.ACHIEVEMENTS')} +

+ + {data.achievementsUnlocked} / {data.achievementsTotal} + +
+
+
+
+
+ {stats.map(([label, value]) => ( +
+
{value}
+
{label}
-
-
- {data.achievementsRemaining} -
-
- {t('GAME_DETAIL.REMAINING')} -
-
-
-
- {data.completionPercent}% -
-
- {t('GAME_DETAIL.COMPLETION')} -
-
-
-
+ ))} +
+ -
-
    - {sortedAchievements.map((a) => ( -
  • - {a.unlocked ? ( - a.iconUrl - ? - :
    - ) : ( - a.iconGrayUrl - ? - :
    +
      + {sortedAchievements.map((a) => { + const icon = a.unlocked ? a.iconUrl : a.iconGrayUrl; + return ( +
    • + {icon + ? + :
      } +
      +
      {a.displayName ?? a.apiName}
      + {a.description && ( +
      {a.description}
      )} -
      -
      - {a.displayName ?? a.apiName} + {a.unlocked && a.unlockedAt && ( +
      + {formatUnlockedAt(a.unlockedAt, i18n.language)}
      - {a.description && ( -
      - {a.description} -
      - )} - {a.unlocked && a.unlockedAt && ( -
      - {formatUnlockedAt(a.unlockedAt, i18n.language)} -
      - )} -
      -
    • - ))} -
    -
- - )} - - )} -
+ )} +
+ + ); + })} + + + )} + + )}
); } diff --git a/src/pages/GameLibrary.jsx b/src/pages/GameLibrary.jsx index 6002354..5bb7e37 100644 --- a/src/pages/GameLibrary.jsx +++ b/src/pages/GameLibrary.jsx @@ -4,7 +4,15 @@ import {useTranslation} from 'react-i18next'; import useLibrarySync from '../hooks/useLibrarySync.js'; import SyncStatusBadge from '../components/SyncStatusBadge.jsx'; import useForceSync from '../hooks/useForceSync.js'; -import {formatPlaytime, iconUrl} from '../utils/games.js'; +import {formatPlaytime} from '../utils/games.js'; +import {ArrowUpDown, ChevronLeft, ChevronRight, Clock, Library, RefreshCw} from 'lucide-react'; + +import {Badge} from '@/components/ui/badge'; +import {Button} from '@/components/ui/button'; +import {Card} from '@/components/ui/card'; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'; +import {Skeleton} from '@/components/ui/skeleton'; +import GameCover from '../components/GameCover.jsx'; const PAGE_SIZES = [20, 40, 80]; const DEFAULT_SIZE = 20; @@ -87,12 +95,12 @@ export default function GameLibrary() { setSearchParams(params); }; - const onSizeChange = (event) => { - updateParams({size: event.target.value, page: 0}); + const onSizeChange = (value) => { + updateParams({size: value, page: 0}); }; - const onSortChange = (event) => { - updateParams({sort: event.target.value, page: 0}); + const onSortChange = (value) => { + updateParams({sort: value, page: 0}); }; const goToPage = (target) => { @@ -100,142 +108,117 @@ export default function GameLibrary() { updateParams({page: target}); }; - const {trigger: triggerSync, busy: syncBusy, message: syncMessage} = useForceSync(load); + const {trigger: triggerSync, busy: syncBusy} = useForceSync(load); + + const syncing = syncBusy || sync.isRunning; + const ready = !isLoading && !error; return ( -
-
-
-
-

- {t('LIBRARY.TITLE')} -

-
- - - {syncMessage && ( - {syncMessage} - )} -
-
-
-
- - -
-
- - -
+
+
+
+

{t('LIBRARY.TITLE')}

+
+ +
- - {isLoading && ( -
- {t('LIBRARY.LOADING')} -
- )} - - {error && ( -
{error}
- )} - - {!isLoading && !error && data.content.length === 0 && ( -
- {sync.isRunning ? t('LIBRARY.SYNCING_FIRST_TIME') : t('LIBRARY.EMPTY')} -
- )} - - {!isLoading && !error && data.content.length > 0 && ( -
    - {data.content.map((g) => { - const icon = iconUrl(g.appid, g.imgIconUrl); - return ( -
  • - - {icon ? ( - { e.currentTarget.style.visibility = 'hidden'; }} - /> - ) : ( -
    - )} - - {g.name ?? `App ${g.appid}`} - - - {formatPlaytime(g.playtimeMinutes)} - - - -
  • - ); - })} -
- )} - - {!isLoading && !error && data.totalPages > 1 && ( -
- - - {t('LIBRARY.PAGE_OF', {current: data.page + 1, total: data.totalPages})} - {' · '} - {data.totalElements} {t('LIBRARY.GAMES')} - - -
- )} +
+ + +
+ + {isLoading && ( + + {Array.from({length: 8}, (_, i) => ( +
+ + + +
+ ))} +
+ )} + + {error &&
{error}
} + + {ready && data.content.length === 0 && ( + + +

+ {sync.isRunning ? t('LIBRARY.SYNCING_FIRST_TIME') : t('LIBRARY.EMPTY')} +

+
+ )} + + {ready && data.content.length > 0 && ( + +
    + {data.content.map((g) => ( +
  • + + + + {g.name ?? `App ${g.appid}`} + + + + {formatPlaytime(g.playtimeMinutes)} + + + +
  • + ))} +
+
+ )} + + {ready && data.totalPages > 1 && ( +
+ + + {t('LIBRARY.PAGE_OF', {current: data.page + 1, total: data.totalPages})} + {' · '} + {data.totalElements} {t('LIBRARY.GAMES')} + + +
+ )}
); } diff --git a/src/pages/ImprintPage.jsx b/src/pages/ImprintPage.jsx index 5d232fd..c52e773 100644 --- a/src/pages/ImprintPage.jsx +++ b/src/pages/ImprintPage.jsx @@ -1,28 +1,30 @@ -import {useTranslation} from 'react-i18next'; +import {useTranslation} from 'react-i18next'; + +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; export default function ImprintPage() { const {t} = useTranslation(); return ( -
-
-

- {t('IMPRINT.TITLE')} -

-
+
+ + + {t('IMPRINT.TITLE')} + +

{t('IMPRINT.LEGAL')}

- Andreas Dahm
+ Andreas Dahm
Hauptstraße 37A
85614 Kirchseeon
Deutschland

-

+

{t('IMPRINT.CONTACT')}

-

Email: andreas.dahm@gmail.com

-
-
+

Email: andreas.dahm@gmail.com

+ +
); } \ No newline at end of file diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index c124551..2819907 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -1,146 +1,105 @@ import {useTranslation} from 'react-i18next'; import {Link} from 'react-router-dom'; -import {getValidToken} from '../utils/auth.js'; +import { + Archive, ArrowRight, Blocks, Clock, Gamepad2, Library, Link2, Sparkles, Trophy, Users, +} from 'lucide-react'; + +import {Badge} from '@/components/ui/badge'; +import {Button} from '@/components/ui/button'; +import {Card} from '@/components/ui/card'; +import {Skeleton} from '@/components/ui/skeleton'; +import GameCover from '../components/GameCover.jsx'; import StatCard from '../components/StatCard.jsx'; +import {getValidToken} from '../utils/auth.js'; import usePublicStats from '../hooks/usePublicStats.js'; -const iconUrl = (appid, hash) => - hash - ? `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${appid}/${hash}.jpg` - : null; +function RankedGames({title, games, formatValue}) { + return ( + +

+ + {title} +

+
    + {games.map((g, idx) => ( +
  1. + {idx + 1} + + {g.name ?? `App ${g.appid}`} + {formatValue(g.value ?? 0)} +
  2. + ))} +
+
+ ); +} export default function LandingPage() { - const {t} = useTranslation(); + const {t, i18n} = useTranslation(); const {data, isLoading} = usePublicStats(); const auth = data.auth; const core = data.core; + const stats = [ + [t('LANDING.TOTAL_USERS'), auth?.totalUsers, Users], + [t('LANDING.STEAM_LINKED'), auth?.usersWithSteamLinked, Link2], + [t('LANDING.TOTAL_GAMES'), core?.totalLibraryEntries, Gamepad2], + [t('LANDING.GAMES_PLAYED'), core?.totalGamesPlayed, Sparkles], + [t('LANDING.DISTINCT_GAMES'), core?.totalDistinctGames, Blocks], + [t('LANDING.TOTAL_PLAYTIME_DAYS'), core?.totalPlaytimeDays, Clock, ` ${t('LANDING.DAYS_SUFFIX')}`], + [t('LANDING.BIGGEST_LIBRARY'), core?.biggestLibrary, Library], + [t('LANDING.BIGGEST_BACKLOG'), core?.biggestBacklog, Archive], + ]; + return ( -
-
-

- {t('MAIN.WELCOME')} -

-

- {t('MAIN.DESCRIPTION')} -

- {getValidToken() ? ( - - {t('NAV.TO_DASHBOARD')} - - ) : ( - - {t('MAIN.GET_STARTED')} - - )} +
+
+
+
+ + Game Log + +

{t('MAIN.WELCOME')}

+

{t('MAIN.DESCRIPTION')}

+ +
-
-

- {t('LANDING.STATS_TITLE')} -

- {isLoading ? ( -
- {t('LANDING.STATS_LOADING')} -
- ) : ( -
- - - - - - - - -
- )} +
+

{t('LANDING.STATS_TITLE')}

- {core?.topPlayedGames?.length > 0 && ( -
-
-

- {t('LANDING.TOP_PLAYED')} -

-
    - {core.topPlayedGames.map((g, idx) => ( -
  1. - {idx + 1} - {iconUrl(g.appid, g.imgIconUrl) ? ( - - ) : ( -
    - )} - - {g.name ?? `App ${g.appid}`} - - - {Math.round(g.value / 60).toLocaleString()}h - -
  2. - ))} -
-
-
-

- {t('LANDING.TOP_OWNED')} -

-
    - {core.topOwnedGames.map((g, idx) => ( -
  1. - {idx + 1} - {iconUrl(g.appid, g.imgIconUrl) ? ( - - ) : ( -
    - )} - - {g.name ?? `App ${g.appid}`} - - - {g.value.toLocaleString()} - -
  2. - ))} -
-
-
- )} +
+ {isLoading + ? stats.map(([label]) => ) + : stats.map(([label, value, icon, suffix]) => ( + + ))} +
+ +
+ {core?.topPlayedGames?.length > 0 && ( + `${Math.round(minutes / 60).toLocaleString(i18n.language)}h`} + /> + )} + {core?.topOwnedGames?.length > 0 && ( + count.toLocaleString(i18n.language)} + /> + )} +
); diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 4368e1c..89b8773 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,6 +1,13 @@ import {useTranslation} from 'react-i18next'; import {useState} from 'react'; import {Link, Navigate, useNavigate} from 'react-router-dom'; +import {Loader2} from 'lucide-react'; + +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import {Label} from '@/components/ui/label'; +import AuthLayout from '../components/AuthLayout.jsx'; +import FeedbackAlert from '../components/FeedbackAlert.jsx'; import {getValidToken} from '../utils/auth.js'; export default function LoginPage() { @@ -64,83 +71,47 @@ export default function LoginPage() { } return ( -
-
+ + {t('LOGIN.NO_ACCOUNT')}{' '} + + {t('REGISTRATION.TITLE')} + + } + > +
+
+ + setEmail(e.target.value)} + required + /> +
-

- {t('LOGIN.TITLE')} -

+
+ + setPassword(e.target.value)} + required + /> +
- + +
- {/* Email Field */} -
- - setEmail(e.target.value)} - className="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none" - required - /> -
- - {/* Password Field */} -
- - setPassword(e.target.value)} - className="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none" - required - /> -
- - {/* Submit Button */} - - - - - {/* Feedback Message Area */} - {feedback.message && ( -
- {feedback.message} -
- )} - -

- {t('LOGIN.NO_ACCOUNT')}{' '} - - {t('REGISTRATION.TITLE')} - -

- -
-
+ + ); } \ No newline at end of file diff --git a/src/pages/RegistrationPage.jsx b/src/pages/RegistrationPage.jsx index 611f738..f0acf58 100644 --- a/src/pages/RegistrationPage.jsx +++ b/src/pages/RegistrationPage.jsx @@ -4,6 +4,13 @@ import {useTranslation} from 'react-i18next'; import {useForm} from 'react-hook-form'; import {zodResolver} from '@hookform/resolvers/zod'; +import {Loader2} from 'lucide-react'; + +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import AuthLayout from '../components/AuthLayout.jsx'; +import FeedbackAlert from '../components/FeedbackAlert.jsx'; +import FormField from '../components/FormField.jsx'; import {registrationSchema} from '../utils/validation.js'; const BACKEND_ERROR_KEY = { @@ -12,11 +19,6 @@ const BACKEND_ERROR_KEY = { STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE', }; -const inputClass = - 'w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none'; -const inputErrorClass = - 'w-full px-4 py-2 border border-red-500 dark:border-red-500 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:outline-none'; - export default function RegistrationPage() { const {t} = useTranslation(); const navigate = useNavigate(); @@ -70,116 +72,73 @@ export default function RegistrationPage() { } }; - const renderError = (field) => - errors[field] && ( -

{t(errors[field].message)}

- ); - return ( -
-
-

- {t('REGISTRATION.TITLE')} -

+ + {t('REGISTRATION.HAVE_ACCOUNT')}{' '} + + {t('MAIN.LOGIN')} + + } + > +
+ + + - -
- - - {renderError('email')} -
+ + + -
- - - {renderError('password')} -
+ + + -
- - - {renderError('passwordConfirmation')} -
+ + + -
- - - {renderError('inviteCode')} -
+ + + -
- - - {renderError('steamId')} -
+ +
- - - - {feedback.message && ( -
- {feedback.message} -
- )} - -

- {t('REGISTRATION.HAVE_ACCOUNT')}{' '} - - {t('MAIN.LOGIN')} - -

-
-
+ + ); } diff --git a/src/pages/SettingsPage.jsx b/src/pages/SettingsPage.jsx index 352b452..1994111 100644 --- a/src/pages/SettingsPage.jsx +++ b/src/pages/SettingsPage.jsx @@ -4,6 +4,17 @@ import {useTranslation} from 'react-i18next'; import {useForm} from 'react-hook-form'; import {zodResolver} from '@hookform/resolvers/zod'; +import {Gamepad2, Loader2, Save, SlidersHorizontal, UserRound} from 'lucide-react'; +import {toast} from 'sonner'; + +import {Button} from '@/components/ui/button'; +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; +import {Input} from '@/components/ui/input'; +import {Label} from '@/components/ui/label'; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'; +import {Skeleton} from '@/components/ui/skeleton'; +import {Switch} from '@/components/ui/switch'; +import FormField from '../components/FormField.jsx'; import {settingsSchema} from '../utils/validation.js'; import {usePreferences} from '../context/PreferencesContext.jsx'; @@ -13,13 +24,6 @@ const BACKEND_ERROR_KEY = { const SUPPORTED_LANGUAGES = ['en', 'de']; -const inputClass = - 'w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none'; -const inputErrorClass = - 'w-full px-4 py-2 border border-red-500 dark:border-red-500 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:outline-none'; -const readOnlyClass = - 'w-full px-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg bg-slate-100 dark:bg-slate-900 text-slate-500 dark:text-slate-400 cursor-not-allowed'; - const formatDate = (value, locale) => { if (!value) return '—'; try { @@ -37,7 +41,6 @@ export default function SettingsPage() { const [profile, setProfile] = useState(null); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); - const [feedback, setFeedback] = useState({message: '', isError: false}); const { register, @@ -121,20 +124,17 @@ export default function SettingsPage() { if (!response.ok) { const data = await response.json().catch(() => ({})); const mappedKey = BACKEND_ERROR_KEY[data.code]; - setFeedback({ - message: mappedKey ? t(mappedKey) : data.message || t('SETTINGS.ERROR_SAVE'), - isError: true, - }); + toast.error(mappedKey ? t(mappedKey) : data.message || t('SETTINGS.ERROR_SAVE')); return {ok: false}; } const data = await response.json(); localStorage.setItem('tbp_jwt', data.token); // Core reads the steamId from the token - setFeedback({message: t('SETTINGS.SAVED'), isError: false}); + toast.success(t('SETTINGS.SAVED')); return {ok: true}; } catch (err) { console.error('Settings save error:', err); - setFeedback({message: t('INFO.ERROR.NETWORK_FAILURE'), isError: true}); + toast.error(t('INFO.ERROR.NETWORK_FAILURE')); return {ok: false}; } }; @@ -149,8 +149,7 @@ export default function SettingsPage() { }); }; - const handleLanguageChange = async (event) => { - const next = event.target.value; + const handleLanguageChange = async (next) => { setLanguage(next); // apply on the fly await persistSettings({ useDarkTheme: isDarkMode, @@ -170,167 +169,98 @@ export default function SettingsPage() { } }; - const renderError = (field) => - errors[field] && ( -

{t(errors[field].message)}

- ); - if (isLoading) { return ( -
- {t('SETTINGS.LOADING')} +
+ + + +
); } if (loadError) { - return ( -
- {loadError} -
- ); + return
{loadError}
; } + const accountRows = [ + [t('SETTINGS.EMAIL'), profile?.email], + [t('SETTINGS.ROLE'), profile?.role], + [t('SETTINGS.CREATED_DATE'), formatDate(profile?.createdDate, i18n.language)], + [t('SETTINGS.LAST_LOGIN'), formatDate(profile?.lastLogin, i18n.language)], + ]; + return ( -
-
-
-

- {t('SETTINGS.TITLE')} -

+
+

{t('SETTINGS.TITLE')}

- {/* Read-only account info */} -
-

- {t('SETTINGS.ACCOUNT')} -

-
- - -
-
-
- - + + + {t('SETTINGS.ACCOUNT')} + + +
+ {accountRows.map(([label, value]) => ( +
+
{label}
+
{value || '—'}
-
- - -
-
-
- - -
-
+ ))} + + + - {/* Preferences — live */} -
-

- {t('SETTINGS.PREFERENCES')} -

- -
- - -
- -
- - + + + + {SUPPORTED_LANGUAGES.map((lang) => ( - + ))} - -
-
+ + +
+ + - {/* Steam ID — explicit save */} -
-

- {t('SETTINGS.STEAM')} -

-
-
- - - {renderError('steamId')} -
- -
-
- - {feedback.message && ( -
- {feedback.message} -
- )} -
-
+ + + {t('SETTINGS.STEAM')} + + +
+ + + + +
+
+
); } diff --git a/src/style.css b/src/style.css index ffc5adb..35360e3 100644 --- a/src/style.css +++ b/src/style.css @@ -1,3 +1,85 @@ @import "tailwindcss"; +@import "tw-animate-css"; +@import "@fontsource-variable/inter"; -@custom-variant dark (&:is(.dark *)); \ No newline at end of file +@custom-variant dark (&:is(.dark *)); + +/* shadcn/ui theme tokens: zinc surfaces, violet accent. */ +:root { + --radius: 0.75rem; + --background: oklch(0.985 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.541 0.281 293.009); + --primary-foreground: oklch(0.969 0.016 293.756); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.943 0.029 294.588); + --accent-foreground: oklch(0.38 0.189 293.745); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.606 0.25 292.717); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.606 0.25 292.717); + --primary-foreground: oklch(0.969 0.016 293.756); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.283 0.141 291.089); + --accent-foreground: oklch(0.894 0.057 293.283); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.541 0.281 293.009); +} + +@theme inline { + --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + + body { + @apply bg-background text-foreground antialiased; + } +} diff --git a/src/utils/games.js b/src/utils/games.js index 954564a..34cd972 100644 --- a/src/utils/games.js +++ b/src/utils/games.js @@ -9,3 +9,6 @@ export const formatPlaytime = (minutes) => { const hours = Math.round(minutes / 6) / 10; return hours >= 10 ? `${Math.round(hours)}h` : `${hours.toFixed(1)}h`; }; + +// 460x215 store header. Not every app has one; callers need an onError fallback. +export const coverUrl = (appid) => `https://cdn.cloudflare.steamstatic.com/steam/apps/${appid}/header.jpg`; diff --git a/vite.config.js b/vite.config.js index 9af89b0..0cf1da5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,3 +1,4 @@ +import path from 'node:path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; @@ -7,4 +8,9 @@ export default defineConfig({ react(), tailwindcss(), ], -}); \ No newline at end of file + resolve: { + alias: { + '@': path.resolve(import.meta.dirname, './src'), + }, + }, +});