wip
| @@ -1,3 +1,3 @@ | |||||||
| # Ignore artifacts: | # Ignore artifacts: | ||||||
| dist | build | ||||||
| node_modules | coverage | ||||||
|   | |||||||
| @@ -1,23 +0,0 @@ | |||||||
| FROM oven/bun:1.1.45-alpine |  | ||||||
|  |  | ||||||
| WORKDIR /frontend |  | ||||||
|  |  | ||||||
| COPY ./frontend/package.json ./ |  | ||||||
| COPY ./frontend/bun.lockb ./ |  | ||||||
|  |  | ||||||
| RUN bun install |  | ||||||
|  |  | ||||||
| COPY ./frontend/public ./public |  | ||||||
| COPY ./frontend/src ./src |  | ||||||
|  |  | ||||||
| COPY ./frontend/eslint.config.js ./ |  | ||||||
| COPY ./frontend/index.html ./ |  | ||||||
| COPY ./frontend/tsconfig.json ./ |  | ||||||
| COPY ./frontend/tsconfig.app.json ./ |  | ||||||
| COPY ./frontend/tsconfig.node.json ./ |  | ||||||
| COPY ./frontend/vite.config.ts ./ |  | ||||||
| COPY ./frontend/postcss.config.cjs ./ |  | ||||||
|  |  | ||||||
| EXPOSE 5173 |  | ||||||
|  |  | ||||||
| ENTRYPOINT ["bun", "run", "dev"] |  | ||||||
							
								
								
									
										54
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | |||||||
|  | # React + TypeScript + Vite | ||||||
|  |  | ||||||
|  | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||||||
|  |  | ||||||
|  | Currently, two official plugins are available: | ||||||
|  |  | ||||||
|  | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh | ||||||
|  | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh | ||||||
|  |  | ||||||
|  | ## Expanding the ESLint configuration | ||||||
|  |  | ||||||
|  | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||||||
|  |  | ||||||
|  | ```js | ||||||
|  | export default tseslint.config({ | ||||||
|  |   extends: [ | ||||||
|  |     // Remove ...tseslint.configs.recommended and replace with this | ||||||
|  |     ...tseslint.configs.recommendedTypeChecked, | ||||||
|  |     // Alternatively, use this for stricter rules | ||||||
|  |     ...tseslint.configs.strictTypeChecked, | ||||||
|  |     // Optionally, add this for stylistic rules | ||||||
|  |     ...tseslint.configs.stylisticTypeChecked, | ||||||
|  |   ], | ||||||
|  |   languageOptions: { | ||||||
|  |     // other options... | ||||||
|  |     parserOptions: { | ||||||
|  |       project: ['./tsconfig.node.json', './tsconfig.app.json'], | ||||||
|  |       tsconfigRootDir: import.meta.dirname, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: | ||||||
|  |  | ||||||
|  | ```js | ||||||
|  | // eslint.config.js | ||||||
|  | import reactX from 'eslint-plugin-react-x' | ||||||
|  | import reactDom from 'eslint-plugin-react-dom' | ||||||
|  |  | ||||||
|  | export default tseslint.config({ | ||||||
|  |   plugins: { | ||||||
|  |     // Add the react-x and react-dom plugins | ||||||
|  |     'react-x': reactX, | ||||||
|  |     'react-dom': reactDom, | ||||||
|  |   }, | ||||||
|  |   rules: { | ||||||
|  |     // other rules... | ||||||
|  |     // Enable its recommended typescript rules | ||||||
|  |     ...reactX.configs['recommended-typescript'].rules, | ||||||
|  |     ...reactDom.configs.recommended.rules, | ||||||
|  |   }, | ||||||
|  | }) | ||||||
|  | ``` | ||||||
							
								
								
									
										727
									
								
								frontend/bun.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,727 @@ | |||||||
|  | { | ||||||
|  |   "lockfileVersion": 1, | ||||||
|  |   "workspaces": { | ||||||
|  |     "": { | ||||||
|  |       "name": "tinyauth-shadcn", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@radix-ui/react-label": "^2.1.4", | ||||||
|  |         "@radix-ui/react-select": "^2.2.2", | ||||||
|  |         "@radix-ui/react-separator": "^1.1.4", | ||||||
|  |         "@radix-ui/react-slot": "^1.2.0", | ||||||
|  |         "@tailwindcss/vite": "^4.1.4", | ||||||
|  |         "class-variance-authority": "^0.7.1", | ||||||
|  |         "clsx": "^2.1.1", | ||||||
|  |         "i18next": "^25.0.2", | ||||||
|  |         "i18next-browser-languagedetector": "^8.0.5", | ||||||
|  |         "i18next-resources-to-backend": "^1.2.1", | ||||||
|  |         "input-otp": "^1.4.2", | ||||||
|  |         "lucide-react": "^0.503.0", | ||||||
|  |         "react": "^19.0.0", | ||||||
|  |         "react-dom": "^19.0.0", | ||||||
|  |         "react-i18next": "^15.5.1", | ||||||
|  |         "react-router": "^7.5.3", | ||||||
|  |         "tailwind-merge": "^3.2.0", | ||||||
|  |         "tailwindcss": "^4.1.4", | ||||||
|  |       }, | ||||||
|  |       "devDependencies": { | ||||||
|  |         "@eslint/js": "^9.22.0", | ||||||
|  |         "@types/node": "^22.15.3", | ||||||
|  |         "@types/react": "^19.0.10", | ||||||
|  |         "@types/react-dom": "^19.0.4", | ||||||
|  |         "@vitejs/plugin-react": "^4.3.4", | ||||||
|  |         "eslint": "^9.22.0", | ||||||
|  |         "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|  |         "eslint-plugin-react-refresh": "^0.4.19", | ||||||
|  |         "globals": "^16.0.0", | ||||||
|  |         "prettier": "3.5.3", | ||||||
|  |         "tw-animate-css": "^1.2.8", | ||||||
|  |         "typescript": "~5.7.2", | ||||||
|  |         "typescript-eslint": "^8.26.1", | ||||||
|  |         "vite": "^6.3.1", | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   "packages": { | ||||||
|  |     "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], | ||||||
|  |  | ||||||
|  |     "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], | ||||||
|  |  | ||||||
|  |     "@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="], | ||||||
|  |  | ||||||
|  |     "@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], | ||||||
|  |  | ||||||
|  |     "@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="], | ||||||
|  |  | ||||||
|  |     "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="], | ||||||
|  |  | ||||||
|  |     "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], | ||||||
|  |  | ||||||
|  |     "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], | ||||||
|  |  | ||||||
|  |     "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], | ||||||
|  |  | ||||||
|  |     "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], | ||||||
|  |  | ||||||
|  |     "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], | ||||||
|  |  | ||||||
|  |     "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], | ||||||
|  |  | ||||||
|  |     "@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="], | ||||||
|  |  | ||||||
|  |     "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], | ||||||
|  |  | ||||||
|  |     "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], | ||||||
|  |  | ||||||
|  |     "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], | ||||||
|  |  | ||||||
|  |     "@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="], | ||||||
|  |  | ||||||
|  |     "@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="], | ||||||
|  |  | ||||||
|  |     "@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], | ||||||
|  |  | ||||||
|  |     "@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/android-arm": ["@esbuild/android-arm@0.25.3", "", { "os": "android", "cpu": "arm" }, "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.3", "", { "os": "android", "cpu": "arm64" }, "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/android-x64": ["@esbuild/android-x64@0.25.3", "", { "os": "android", "cpu": "x64" }, "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.3", "", { "os": "linux", "cpu": "arm" }, "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.3", "", { "os": "linux", "cpu": "x64" }, "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.3", "", { "os": "none", "cpu": "arm64" }, "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.3", "", { "os": "none", "cpu": "x64" }, "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew=="], | ||||||
|  |  | ||||||
|  |     "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg=="], | ||||||
|  |  | ||||||
|  |     "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.6.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw=="], | ||||||
|  |  | ||||||
|  |     "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], | ||||||
|  |  | ||||||
|  |     "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="], | ||||||
|  |  | ||||||
|  |     "@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="], | ||||||
|  |  | ||||||
|  |     "@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="], | ||||||
|  |  | ||||||
|  |     "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], | ||||||
|  |  | ||||||
|  |     "@eslint/js": ["@eslint/js@9.25.1", "", {}, "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg=="], | ||||||
|  |  | ||||||
|  |     "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], | ||||||
|  |  | ||||||
|  |     "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="], | ||||||
|  |  | ||||||
|  |     "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="], | ||||||
|  |  | ||||||
|  |     "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], | ||||||
|  |  | ||||||
|  |     "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], | ||||||
|  |  | ||||||
|  |     "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], | ||||||
|  |  | ||||||
|  |     "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], | ||||||
|  |  | ||||||
|  |     "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], | ||||||
|  |  | ||||||
|  |     "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], | ||||||
|  |  | ||||||
|  |     "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], | ||||||
|  |  | ||||||
|  |     "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], | ||||||
|  |  | ||||||
|  |     "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], | ||||||
|  |  | ||||||
|  |     "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], | ||||||
|  |  | ||||||
|  |     "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], | ||||||
|  |  | ||||||
|  |     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], | ||||||
|  |  | ||||||
|  |     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], | ||||||
|  |  | ||||||
|  |     "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], | ||||||
|  |  | ||||||
|  |     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-label": ["@radix-ui/react-label@2.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-select": ["@radix-ui/react-select@2.2.2", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg=="], | ||||||
|  |  | ||||||
|  |     "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.1", "", { "os": "android", "cpu": "arm" }, "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.1", "", { "os": "android", "cpu": "arm64" }, "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.1", "", { "os": "linux", "cpu": "arm" }, "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.1", "", { "os": "linux", "cpu": "none" }, "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.1", "", { "os": "linux", "cpu": "none" }, "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.1", "", { "os": "linux", "cpu": "none" }, "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA=="], | ||||||
|  |  | ||||||
|  |     "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/node": ["@tailwindcss/node@4.1.4", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.4" } }, "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.4", "@tailwindcss/oxide-darwin-arm64": "4.1.4", "@tailwindcss/oxide-darwin-x64": "4.1.4", "@tailwindcss/oxide-freebsd-x64": "4.1.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", "@tailwindcss/oxide-linux-x64-musl": "4.1.4", "@tailwindcss/oxide-wasm32-wasi": "4.1.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.4", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@emnapi/wasi-threads": "^1.0.1", "@napi-rs/wasm-runtime": "^0.2.8", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/vite": ["@tailwindcss/vite@4.1.4", "", { "dependencies": { "@tailwindcss/node": "4.1.4", "@tailwindcss/oxide": "4.1.4", "tailwindcss": "4.1.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A=="], | ||||||
|  |  | ||||||
|  |     "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], | ||||||
|  |  | ||||||
|  |     "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], | ||||||
|  |  | ||||||
|  |     "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], | ||||||
|  |  | ||||||
|  |     "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], | ||||||
|  |  | ||||||
|  |     "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], | ||||||
|  |  | ||||||
|  |     "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], | ||||||
|  |  | ||||||
|  |     "@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="], | ||||||
|  |  | ||||||
|  |     "@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="], | ||||||
|  |  | ||||||
|  |     "@types/react-dom": ["@types/react-dom@19.1.2", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.31.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/type-utils": "8.31.1", "@typescript-eslint/utils": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/parser": ["@typescript-eslint/parser@8.31.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/types": "8.31.1", "@typescript-eslint/typescript-estree": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.31.1", "", { "dependencies": { "@typescript-eslint/types": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1" } }, "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.31.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.31.1", "@typescript-eslint/utils": "8.31.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/types": ["@typescript-eslint/types@8.31.1", "", {}, "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.31.1", "", { "dependencies": { "@typescript-eslint/types": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/utils": ["@typescript-eslint/utils@8.31.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/types": "8.31.1", "@typescript-eslint/typescript-estree": "8.31.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.31.1", "", { "dependencies": { "@typescript-eslint/types": "8.31.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw=="], | ||||||
|  |  | ||||||
|  |     "@vitejs/plugin-react": ["@vitejs/plugin-react@4.4.1", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w=="], | ||||||
|  |  | ||||||
|  |     "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], | ||||||
|  |  | ||||||
|  |     "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], | ||||||
|  |  | ||||||
|  |     "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], | ||||||
|  |  | ||||||
|  |     "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], | ||||||
|  |  | ||||||
|  |     "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], | ||||||
|  |  | ||||||
|  |     "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], | ||||||
|  |  | ||||||
|  |     "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], | ||||||
|  |  | ||||||
|  |     "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], | ||||||
|  |  | ||||||
|  |     "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], | ||||||
|  |  | ||||||
|  |     "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], | ||||||
|  |  | ||||||
|  |     "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], | ||||||
|  |  | ||||||
|  |     "caniuse-lite": ["caniuse-lite@1.0.30001715", "", {}, "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw=="], | ||||||
|  |  | ||||||
|  |     "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], | ||||||
|  |  | ||||||
|  |     "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], | ||||||
|  |  | ||||||
|  |     "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], | ||||||
|  |  | ||||||
|  |     "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], | ||||||
|  |  | ||||||
|  |     "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], | ||||||
|  |  | ||||||
|  |     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], | ||||||
|  |  | ||||||
|  |     "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], | ||||||
|  |  | ||||||
|  |     "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], | ||||||
|  |  | ||||||
|  |     "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], | ||||||
|  |  | ||||||
|  |     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], | ||||||
|  |  | ||||||
|  |     "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], | ||||||
|  |  | ||||||
|  |     "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], | ||||||
|  |  | ||||||
|  |     "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], | ||||||
|  |  | ||||||
|  |     "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], | ||||||
|  |  | ||||||
|  |     "electron-to-chromium": ["electron-to-chromium@1.5.144", "", {}, "sha512-eJIaMRKeAzxfBSxtjYnoIAw/tdD6VIH6tHBZepZnAbE3Gyqqs5mGN87DvcldPUbVkIljTK8pY0CMcUljP64lfQ=="], | ||||||
|  |  | ||||||
|  |     "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], | ||||||
|  |  | ||||||
|  |     "esbuild": ["esbuild@0.25.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.3", "@esbuild/android-arm": "0.25.3", "@esbuild/android-arm64": "0.25.3", "@esbuild/android-x64": "0.25.3", "@esbuild/darwin-arm64": "0.25.3", "@esbuild/darwin-x64": "0.25.3", "@esbuild/freebsd-arm64": "0.25.3", "@esbuild/freebsd-x64": "0.25.3", "@esbuild/linux-arm": "0.25.3", "@esbuild/linux-arm64": "0.25.3", "@esbuild/linux-ia32": "0.25.3", "@esbuild/linux-loong64": "0.25.3", "@esbuild/linux-mips64el": "0.25.3", "@esbuild/linux-ppc64": "0.25.3", "@esbuild/linux-riscv64": "0.25.3", "@esbuild/linux-s390x": "0.25.3", "@esbuild/linux-x64": "0.25.3", "@esbuild/netbsd-arm64": "0.25.3", "@esbuild/netbsd-x64": "0.25.3", "@esbuild/openbsd-arm64": "0.25.3", "@esbuild/openbsd-x64": "0.25.3", "@esbuild/sunos-x64": "0.25.3", "@esbuild/win32-arm64": "0.25.3", "@esbuild/win32-ia32": "0.25.3", "@esbuild/win32-x64": "0.25.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q=="], | ||||||
|  |  | ||||||
|  |     "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], | ||||||
|  |  | ||||||
|  |     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], | ||||||
|  |  | ||||||
|  |     "eslint": ["eslint@9.25.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.25.1", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ=="], | ||||||
|  |  | ||||||
|  |     "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], | ||||||
|  |  | ||||||
|  |     "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="], | ||||||
|  |  | ||||||
|  |     "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], | ||||||
|  |  | ||||||
|  |     "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], | ||||||
|  |  | ||||||
|  |     "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], | ||||||
|  |  | ||||||
|  |     "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], | ||||||
|  |  | ||||||
|  |     "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], | ||||||
|  |  | ||||||
|  |     "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], | ||||||
|  |  | ||||||
|  |     "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], | ||||||
|  |  | ||||||
|  |     "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], | ||||||
|  |  | ||||||
|  |     "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], | ||||||
|  |  | ||||||
|  |     "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], | ||||||
|  |  | ||||||
|  |     "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], | ||||||
|  |  | ||||||
|  |     "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], | ||||||
|  |  | ||||||
|  |     "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], | ||||||
|  |  | ||||||
|  |     "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], | ||||||
|  |  | ||||||
|  |     "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], | ||||||
|  |  | ||||||
|  |     "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], | ||||||
|  |  | ||||||
|  |     "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], | ||||||
|  |  | ||||||
|  |     "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], | ||||||
|  |  | ||||||
|  |     "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | ||||||
|  |  | ||||||
|  |     "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], | ||||||
|  |  | ||||||
|  |     "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], | ||||||
|  |  | ||||||
|  |     "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], | ||||||
|  |  | ||||||
|  |     "globals": ["globals@16.0.0", "", {}, "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A=="], | ||||||
|  |  | ||||||
|  |     "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | ||||||
|  |  | ||||||
|  |     "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], | ||||||
|  |  | ||||||
|  |     "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], | ||||||
|  |  | ||||||
|  |     "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], | ||||||
|  |  | ||||||
|  |     "i18next": ["i18next@25.0.2", "", { "dependencies": { "@babel/runtime": "^7.26.10" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-xWxgK8GAaPYkV9ia2tdgbtdM+qiC+ysVTBPvXhpCORU/+QkeQe3BSI7Crr+c4ZXULN1PfnXG/HY2n7HGx4KKBg=="], | ||||||
|  |  | ||||||
|  |     "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.0.5", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-OstebRKqKiQw8xEvQF5aRyUujsCatanj7Q9eo5iiH2gJpoXGZ7483ol3sVBwfqbobTQPNH1J+NAyJ1aCQoEC+w=="], | ||||||
|  |  | ||||||
|  |     "i18next-resources-to-backend": ["i18next-resources-to-backend@1.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw=="], | ||||||
|  |  | ||||||
|  |     "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], | ||||||
|  |  | ||||||
|  |     "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], | ||||||
|  |  | ||||||
|  |     "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], | ||||||
|  |  | ||||||
|  |     "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], | ||||||
|  |  | ||||||
|  |     "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], | ||||||
|  |  | ||||||
|  |     "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], | ||||||
|  |  | ||||||
|  |     "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], | ||||||
|  |  | ||||||
|  |     "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], | ||||||
|  |  | ||||||
|  |     "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], | ||||||
|  |  | ||||||
|  |     "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], | ||||||
|  |  | ||||||
|  |     "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], | ||||||
|  |  | ||||||
|  |     "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], | ||||||
|  |  | ||||||
|  |     "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], | ||||||
|  |  | ||||||
|  |     "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], | ||||||
|  |  | ||||||
|  |     "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], | ||||||
|  |  | ||||||
|  |     "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], | ||||||
|  |  | ||||||
|  |     "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], | ||||||
|  |  | ||||||
|  |     "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], | ||||||
|  |  | ||||||
|  |     "lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="], | ||||||
|  |  | ||||||
|  |     "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], | ||||||
|  |  | ||||||
|  |     "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], | ||||||
|  |  | ||||||
|  |     "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], | ||||||
|  |  | ||||||
|  |     "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | ||||||
|  |  | ||||||
|  |     "lucide-react": ["lucide-react@0.503.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w=="], | ||||||
|  |  | ||||||
|  |     "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], | ||||||
|  |  | ||||||
|  |     "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], | ||||||
|  |  | ||||||
|  |     "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], | ||||||
|  |  | ||||||
|  |     "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], | ||||||
|  |  | ||||||
|  |     "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | ||||||
|  |  | ||||||
|  |     "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], | ||||||
|  |  | ||||||
|  |     "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], | ||||||
|  |  | ||||||
|  |     "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], | ||||||
|  |  | ||||||
|  |     "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], | ||||||
|  |  | ||||||
|  |     "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], | ||||||
|  |  | ||||||
|  |     "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], | ||||||
|  |  | ||||||
|  |     "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], | ||||||
|  |  | ||||||
|  |     "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], | ||||||
|  |  | ||||||
|  |     "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | ||||||
|  |  | ||||||
|  |     "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], | ||||||
|  |  | ||||||
|  |     "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], | ||||||
|  |  | ||||||
|  |     "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], | ||||||
|  |  | ||||||
|  |     "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], | ||||||
|  |  | ||||||
|  |     "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], | ||||||
|  |  | ||||||
|  |     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], | ||||||
|  |  | ||||||
|  |     "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], | ||||||
|  |  | ||||||
|  |     "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], | ||||||
|  |  | ||||||
|  |     "react-i18next": ["react-i18next@15.5.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA=="], | ||||||
|  |  | ||||||
|  |     "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], | ||||||
|  |  | ||||||
|  |     "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "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" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], | ||||||
|  |  | ||||||
|  |     "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], | ||||||
|  |  | ||||||
|  |     "react-router": ["react-router@7.5.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw=="], | ||||||
|  |  | ||||||
|  |     "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], | ||||||
|  |  | ||||||
|  |     "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], | ||||||
|  |  | ||||||
|  |     "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], | ||||||
|  |  | ||||||
|  |     "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], | ||||||
|  |  | ||||||
|  |     "rollup": ["rollup@4.40.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.1", "@rollup/rollup-android-arm64": "4.40.1", "@rollup/rollup-darwin-arm64": "4.40.1", "@rollup/rollup-darwin-x64": "4.40.1", "@rollup/rollup-freebsd-arm64": "4.40.1", "@rollup/rollup-freebsd-x64": "4.40.1", "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", "@rollup/rollup-linux-arm-musleabihf": "4.40.1", "@rollup/rollup-linux-arm64-gnu": "4.40.1", "@rollup/rollup-linux-arm64-musl": "4.40.1", "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", "@rollup/rollup-linux-riscv64-gnu": "4.40.1", "@rollup/rollup-linux-riscv64-musl": "4.40.1", "@rollup/rollup-linux-s390x-gnu": "4.40.1", "@rollup/rollup-linux-x64-gnu": "4.40.1", "@rollup/rollup-linux-x64-musl": "4.40.1", "@rollup/rollup-win32-arm64-msvc": "4.40.1", "@rollup/rollup-win32-ia32-msvc": "4.40.1", "@rollup/rollup-win32-x64-msvc": "4.40.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw=="], | ||||||
|  |  | ||||||
|  |     "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], | ||||||
|  |  | ||||||
|  |     "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], | ||||||
|  |  | ||||||
|  |     "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | ||||||
|  |  | ||||||
|  |     "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], | ||||||
|  |  | ||||||
|  |     "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], | ||||||
|  |  | ||||||
|  |     "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], | ||||||
|  |  | ||||||
|  |     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | ||||||
|  |  | ||||||
|  |     "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], | ||||||
|  |  | ||||||
|  |     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], | ||||||
|  |  | ||||||
|  |     "tailwind-merge": ["tailwind-merge@3.2.0", "", {}, "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA=="], | ||||||
|  |  | ||||||
|  |     "tailwindcss": ["tailwindcss@4.1.4", "", {}, "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A=="], | ||||||
|  |  | ||||||
|  |     "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], | ||||||
|  |  | ||||||
|  |     "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], | ||||||
|  |  | ||||||
|  |     "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], | ||||||
|  |  | ||||||
|  |     "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], | ||||||
|  |  | ||||||
|  |     "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="], | ||||||
|  |  | ||||||
|  |     "tw-animate-css": ["tw-animate-css@1.2.8", "", {}, "sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g=="], | ||||||
|  |  | ||||||
|  |     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], | ||||||
|  |  | ||||||
|  |     "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], | ||||||
|  |  | ||||||
|  |     "typescript-eslint": ["typescript-eslint@8.31.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", "@typescript-eslint/utils": "8.31.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA=="], | ||||||
|  |  | ||||||
|  |     "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], | ||||||
|  |  | ||||||
|  |     "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], | ||||||
|  |  | ||||||
|  |     "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], | ||||||
|  |  | ||||||
|  |     "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], | ||||||
|  |  | ||||||
|  |     "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], | ||||||
|  |  | ||||||
|  |     "vite": ["vite@6.3.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw=="], | ||||||
|  |  | ||||||
|  |     "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], | ||||||
|  |  | ||||||
|  |     "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], | ||||||
|  |  | ||||||
|  |     "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], | ||||||
|  |  | ||||||
|  |     "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | ||||||
|  |  | ||||||
|  |     "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], | ||||||
|  |  | ||||||
|  |     "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], | ||||||
|  |  | ||||||
|  |     "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], | ||||||
|  |  | ||||||
|  |     "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], | ||||||
|  |  | ||||||
|  |     "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.9", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], | ||||||
|  |  | ||||||
|  |     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], | ||||||
|  |  | ||||||
|  |     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								frontend/components.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "https://ui.shadcn.com/schema.json", | ||||||
|  |   "style": "new-york", | ||||||
|  |   "rsc": false, | ||||||
|  |   "tsx": true, | ||||||
|  |   "tailwind": { | ||||||
|  |     "config": "", | ||||||
|  |     "css": "src/index.css", | ||||||
|  |     "baseColor": "neutral", | ||||||
|  |     "cssVariables": true, | ||||||
|  |     "prefix": "" | ||||||
|  |   }, | ||||||
|  |   "aliases": { | ||||||
|  |     "components": "@/components", | ||||||
|  |     "utils": "@/lib/utils", | ||||||
|  |     "ui": "@/components/ui", | ||||||
|  |     "lib": "@/lib", | ||||||
|  |     "hooks": "@/hooks" | ||||||
|  |   }, | ||||||
|  |   "iconLibrary": "lucide" | ||||||
|  | } | ||||||
| @@ -1,28 +1,28 @@ | |||||||
| import js from "@eslint/js"; | import js from '@eslint/js' | ||||||
| import globals from "globals"; | import globals from 'globals' | ||||||
| import reactHooks from "eslint-plugin-react-hooks"; | import reactHooks from 'eslint-plugin-react-hooks' | ||||||
| import reactRefresh from "eslint-plugin-react-refresh"; | import reactRefresh from 'eslint-plugin-react-refresh' | ||||||
| import tseslint from "typescript-eslint"; | import tseslint from 'typescript-eslint' | ||||||
|  |  | ||||||
| export default tseslint.config( | export default tseslint.config( | ||||||
|   { ignores: ["dist"] }, |   { ignores: ['dist'] }, | ||||||
|   { |   { | ||||||
|     extends: [js.configs.recommended, ...tseslint.configs.recommended], |     extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||||||
|     files: ["**/*.{ts,tsx}"], |     files: ['**/*.{ts,tsx}'], | ||||||
|     languageOptions: { |     languageOptions: { | ||||||
|       ecmaVersion: 2020, |       ecmaVersion: 2020, | ||||||
|       globals: globals.browser, |       globals: globals.browser, | ||||||
|     }, |     }, | ||||||
|     plugins: { |     plugins: { | ||||||
|       "react-hooks": reactHooks, |       'react-hooks': reactHooks, | ||||||
|       "react-refresh": reactRefresh, |       'react-refresh': reactRefresh, | ||||||
|     }, |     }, | ||||||
|     rules: { |     rules: { | ||||||
|       ...reactHooks.configs.recommended.rules, |       ...reactHooks.configs.recommended.rules, | ||||||
|       "react-refresh/only-export-components": [ |       'react-refresh/only-export-components': [ | ||||||
|         "warn", |         'warn', | ||||||
|         { allowConstantExport: true }, |         { allowConstantExport: true }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| ); | ) | ||||||
|   | |||||||
| @@ -3,13 +3,9 @@ | |||||||
|   <head> |   <head> | ||||||
|     <meta charset="UTF-8" /> |     <meta charset="UTF-8" /> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> |     <title>Tinyauth (Shadcn)</title> | ||||||
|     <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> |  | ||||||
|     <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> |  | ||||||
|     <link rel="manifest" href="/frontend.webmanifest" /> |  | ||||||
|     <title>Tinyauth</title> |  | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body class="dark"> | ||||||
|     <div id="root"></div> |     <div id="root"></div> | ||||||
|     <script type="module" src="/src/main.tsx"></script> |     <script type="module" src="/src/main.tsx"></script> | ||||||
|   </body> |   </body> | ||||||
|   | |||||||
							
								
								
									
										3907
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|   "name": "frontend", |   "name": "tinyauth-shadcn", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "version": "0.0.0", |   "version": "0.0.0", | ||||||
|   "type": "module", |   "type": "module", | ||||||
| @@ -10,39 +10,39 @@ | |||||||
|     "preview": "vite preview" |     "preview": "vite preview" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@mantine/core": "^7.16.0", |     "@radix-ui/react-label": "^2.1.4", | ||||||
|     "@mantine/form": "^7.16.0", |     "@radix-ui/react-select": "^2.2.2", | ||||||
|     "@mantine/hooks": "^7.16.0", |     "@radix-ui/react-separator": "^1.1.4", | ||||||
|     "@mantine/notifications": "^7.16.0", |     "@radix-ui/react-slot": "^1.2.0", | ||||||
|     "@tanstack/react-query": "5", |     "@tailwindcss/vite": "^4.1.4", | ||||||
|     "axios": "^1.7.9", |     "class-variance-authority": "^0.7.1", | ||||||
|     "i18next": "^25.0.0", |     "clsx": "^2.1.1", | ||||||
|     "i18next-browser-languagedetector": "^8.0.4", |     "i18next": "^25.0.2", | ||||||
|     "i18next-chained-backend": "^4.6.2", |     "i18next-browser-languagedetector": "^8.0.5", | ||||||
|     "i18next-http-backend": "^3.0.2", |  | ||||||
|     "i18next-resources-to-backend": "^1.2.1", |     "i18next-resources-to-backend": "^1.2.1", | ||||||
|     "react": "^19.1.0", |     "input-otp": "^1.4.2", | ||||||
|     "react-dom": "^19.1.0", |     "lucide-react": "^0.503.0", | ||||||
|     "react-i18next": "^15.4.1", |     "react": "^19.0.0", | ||||||
|     "react-markdown": "^10.1.0", |     "react-dom": "^19.0.0", | ||||||
|     "react-router": "^7.5.2", |     "react-i18next": "^15.5.1", | ||||||
|     "zod": "^3.24.1" |     "react-router": "^7.5.3", | ||||||
|  |     "tailwind-merge": "^3.2.0", | ||||||
|  |     "tailwindcss": "^4.1.4" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@eslint/js": "^9.17.0", |     "@eslint/js": "^9.22.0", | ||||||
|     "@types/react": "^19.1.1", |     "@types/node": "^22.15.3", | ||||||
|     "@types/react-dom": "^19.1.2", |     "@types/react": "^19.0.10", | ||||||
|     "@vitejs/plugin-react-swc": "^3.5.0", |     "@types/react-dom": "^19.0.4", | ||||||
|     "eslint": "^9.17.0", |     "@vitejs/plugin-react": "^4.3.4", | ||||||
|     "eslint-plugin-react-hooks": "^5.0.0", |     "eslint": "^9.22.0", | ||||||
|     "eslint-plugin-react-refresh": "^0.4.16", |     "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|  |     "eslint-plugin-react-refresh": "^0.4.19", | ||||||
|     "globals": "^16.0.0", |     "globals": "^16.0.0", | ||||||
|     "postcss": "^8.5.1", |  | ||||||
|     "postcss-preset-mantine": "^1.17.0", |  | ||||||
|     "postcss-simple-vars": "^7.0.1", |  | ||||||
|     "prettier": "3.5.3", |     "prettier": "3.5.3", | ||||||
|     "typescript": "~5.8.3", |     "tw-animate-css": "^1.2.8", | ||||||
|     "typescript-eslint": "^8.18.2", |     "typescript": "~5.7.2", | ||||||
|     "vite": "^6.0.5" |     "typescript-eslint": "^8.26.1", | ||||||
|  |     "vite": "^6.3.1" | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| module.exports = { |  | ||||||
|   plugins: { |  | ||||||
|     "postcss-preset-mantine": {}, |  | ||||||
|     "postcss-simple-vars": { |  | ||||||
|       variables: { |  | ||||||
|         "mantine-breakpoint-xs": "36em", |  | ||||||
|         "mantine-breakpoint-sm": "48em", |  | ||||||
|         "mantine-breakpoint-md": "62em", |  | ||||||
|         "mantine-breakpoint-lg": "75em", |  | ||||||
|         "mantine-breakpoint-xl": "88em", |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| Before Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 13 KiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/public/background.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 588 KiB | 
| Before Width: | Height: | Size: 602 B | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 35 KiB | 
| @@ -1 +0,0 @@ | |||||||
| {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} |  | ||||||
| @@ -1,17 +1,5 @@ | |||||||
| import { Navigate } from "react-router"; | import { Navigate } from "react-router"; | ||||||
| import { useUserContext } from "./context/user-context"; |  | ||||||
| import { LogoutPage } from "./pages/logout-page"; |  | ||||||
|  |  | ||||||
| export const App = () => { | export const App = () => { | ||||||
|   const queryString = window.location.search; |   return <Navigate to="/login" />; | ||||||
|   const params = new URLSearchParams(queryString); |  | ||||||
|   const redirectUri = params.get("redirect_uri"); |  | ||||||
|  |  | ||||||
|   const { isLoggedIn } = useUserContext(); |  | ||||||
|  |  | ||||||
|   if (!isLoggedIn) { |  | ||||||
|     return <Navigate to={`/login?redirect_uri=${redirectUri}`} />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return <LogoutPage />; |  | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,57 +0,0 @@ | |||||||
| import { TextInput, PasswordInput, Button, Anchor, Group, Text } from "@mantine/core"; |  | ||||||
| import { useForm, zodResolver } from "@mantine/form"; |  | ||||||
| import { LoginFormValues, loginSchema } from "../../schemas/login-schema"; |  | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
|  |  | ||||||
| interface LoginFormProps { |  | ||||||
|   isPending: boolean; |  | ||||||
|   onSubmit: (values: LoginFormValues) => void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const LoginForm = (props: LoginFormProps) => { |  | ||||||
|   const { isPending, onSubmit } = props; |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|  |  | ||||||
|   const form = useForm({ |  | ||||||
|     mode: "uncontrolled", |  | ||||||
|     initialValues: { |  | ||||||
|       username: "", |  | ||||||
|       password: "", |  | ||||||
|     }, |  | ||||||
|     validate: zodResolver(loginSchema), |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <form onSubmit={form.onSubmit(onSubmit)}> |  | ||||||
|       <TextInput |  | ||||||
|         label={t("loginUsername")} |  | ||||||
|         placeholder="Username" |  | ||||||
|         disabled={isPending} |  | ||||||
|         required |  | ||||||
|         withAsterisk={false} |  | ||||||
|         key={form.key("username")} |  | ||||||
|         {...form.getInputProps("username")} |  | ||||||
|       /> |  | ||||||
|       <Group justify="space-between" mb={5} mt="md"> |  | ||||||
|         <Text component="label" htmlFor=".password-input" size="sm" fw={500}> |  | ||||||
|         {t("loginPassword")} |  | ||||||
|         </Text> |  | ||||||
|  |  | ||||||
|         <Anchor href="#" onClick={() => window.location.replace("/forgot-password")} pt={2} fw={500} fz="xs"> |  | ||||||
|           {t('forgotPasswordTitle')} |  | ||||||
|         </Anchor> |  | ||||||
|       </Group> |  | ||||||
|       <PasswordInput |  | ||||||
|         className="password-input" |  | ||||||
|         placeholder="Password" |  | ||||||
|         required |  | ||||||
|         disabled={isPending} |  | ||||||
|         key={form.key("password")} |  | ||||||
|         {...form.getInputProps("password")} |  | ||||||
|       /> |  | ||||||
|       <Button fullWidth mt="xl" type="submit" loading={isPending}> |  | ||||||
|         {t("loginSubmit")} |  | ||||||
|       </Button> |  | ||||||
|     </form> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
							
								
								
									
										31
									
								
								frontend/src/components/auth/oauth-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | |||||||
|  | import { Loader2 } from "lucide-react"; | ||||||
|  | import { Button } from "../ui/button"; | ||||||
|  | import React from "react"; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  |   title: string; | ||||||
|  |   icon: React.ReactNode; | ||||||
|  |   onClick?: () => void; | ||||||
|  |   loading?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const OAuthButton = (props: Props) => { | ||||||
|  |   const { title, icon, onClick, loading } = props; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Button | ||||||
|  |       onClick={onClick} | ||||||
|  |       className="rounded-full basis-1/3" | ||||||
|  |       variant="outline" | ||||||
|  |     > | ||||||
|  |       {loading ? ( | ||||||
|  |         <Loader2 className="animate-spin" /> | ||||||
|  |       ) : ( | ||||||
|  |         <> | ||||||
|  |           {icon} | ||||||
|  |           {title} | ||||||
|  |         </> | ||||||
|  |       )} | ||||||
|  |     </Button> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| import { Grid, Button } from "@mantine/core"; |  | ||||||
| import { GithubIcon } from "../../icons/github"; |  | ||||||
| import { GoogleIcon } from "../../icons/google"; |  | ||||||
| import { OAuthIcon } from "../../icons/oauth"; |  | ||||||
|  |  | ||||||
| interface OAuthButtonsProps { |  | ||||||
|   oauthProviders: string[]; |  | ||||||
|   isPending: boolean; |  | ||||||
|   mutate: (provider: string) => void; |  | ||||||
|   genericName: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const OAuthButtons = (props: OAuthButtonsProps) => { |  | ||||||
|   const { oauthProviders, isPending, genericName, mutate } = props; |  | ||||||
|   return ( |  | ||||||
|     <Grid mb="md" mt="md" align="center" justify="center"> |  | ||||||
|       {oauthProviders.includes("google") && ( |  | ||||||
|         <Grid.Col span="content"> |  | ||||||
|           <Button |  | ||||||
|             radius="xl" |  | ||||||
|             leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />} |  | ||||||
|             variant="default" |  | ||||||
|             onClick={() => mutate("google")} |  | ||||||
|             loading={isPending} |  | ||||||
|           > |  | ||||||
|             Google |  | ||||||
|           </Button> |  | ||||||
|         </Grid.Col> |  | ||||||
|       )} |  | ||||||
|       {oauthProviders.includes("github") && ( |  | ||||||
|         <Grid.Col span="content"> |  | ||||||
|           <Button |  | ||||||
|             radius="xl" |  | ||||||
|             leftSection={<GithubIcon style={{ width: 14, height: 14 }} />} |  | ||||||
|             variant="default" |  | ||||||
|             onClick={() => mutate("github")} |  | ||||||
|             loading={isPending} |  | ||||||
|           > |  | ||||||
|             Github |  | ||||||
|           </Button> |  | ||||||
|         </Grid.Col> |  | ||||||
|       )} |  | ||||||
|       {oauthProviders.includes("generic") && ( |  | ||||||
|         <Grid.Col span="content"> |  | ||||||
|           <Button |  | ||||||
|             radius="xl" |  | ||||||
|             leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />} |  | ||||||
|             variant="default" |  | ||||||
|             onClick={() => mutate("generic")} |  | ||||||
|             loading={isPending} |  | ||||||
|           > |  | ||||||
|             {genericName} |  | ||||||
|           </Button> |  | ||||||
|         </Grid.Col> |  | ||||||
|       )} |  | ||||||
|     </Grid> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,40 +0,0 @@ | |||||||
| import { Button, PinInput } from "@mantine/core"; |  | ||||||
| import { useForm, zodResolver } from "@mantine/form"; |  | ||||||
| import { z } from "zod"; |  | ||||||
|  |  | ||||||
| const schema = z.object({ |  | ||||||
|   code: z.string(), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| type FormValues = z.infer<typeof schema>; |  | ||||||
|  |  | ||||||
| interface TotpFormProps { |  | ||||||
|   onSubmit: (values: FormValues) => void; |  | ||||||
|   isPending: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const TotpForm = (props: TotpFormProps) => { |  | ||||||
|   const { onSubmit, isPending } = props; |  | ||||||
|  |  | ||||||
|   const form = useForm({ |  | ||||||
|     mode: "uncontrolled", |  | ||||||
|     initialValues: { |  | ||||||
|       code: "", |  | ||||||
|     }, |  | ||||||
|     validate: zodResolver(schema), |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <form onSubmit={form.onSubmit(onSubmit)}> |  | ||||||
|       <PinInput |  | ||||||
|         length={6} |  | ||||||
|         type={"number"} |  | ||||||
|         placeholder="" |  | ||||||
|         {...form.getInputProps("code")} |  | ||||||
|       /> |  | ||||||
|       <Button type="submit" mt="xl" loading={isPending} fullWidth> |  | ||||||
|         Verify |  | ||||||
|       </Button> |  | ||||||
|     </form> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| import type { SVGProps } from "react"; | import type { SVGProps } from "react"; | ||||||
| 
 | 
 | ||||||
| export function OAuthIcon(props: SVGProps<SVGSVGElement>) { | export function GenericIcon(props: SVGProps<SVGSVGElement>) { | ||||||
|   return ( |   return ( | ||||||
|     <svg |     <svg | ||||||
|       xmlns="http://www.w3.org/2000/svg" |       xmlns="http://www.w3.org/2000/svg" | ||||||
							
								
								
									
										30
									
								
								frontend/src/components/icons/google.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | |||||||
|  | import type { SVGProps } from "react"; | ||||||
|  |  | ||||||
|  | export function GoogleIcon(props: SVGProps<SVGSVGElement>) { | ||||||
|  |   return ( | ||||||
|  |     <svg | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       width={256} | ||||||
|  |       height={262} | ||||||
|  |       viewBox="0 0 256 262" | ||||||
|  |       {...props} | ||||||
|  |     > | ||||||
|  |       <path | ||||||
|  |         fill="#4285f4" | ||||||
|  |         d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" | ||||||
|  |       ></path> | ||||||
|  |       <path | ||||||
|  |         fill="#34a853" | ||||||
|  |         d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" | ||||||
|  |       ></path> | ||||||
|  |       <path | ||||||
|  |         fill="#fbbc05" | ||||||
|  |         d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z" | ||||||
|  |       ></path> | ||||||
|  |       <path | ||||||
|  |         fill="#eb4335" | ||||||
|  |         d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" | ||||||
|  |       ></path> | ||||||
|  |     </svg> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -1,40 +0,0 @@ | |||||||
| import { ComboboxItem, Select } from "@mantine/core"; |  | ||||||
| import { useState } from "react"; |  | ||||||
| import i18n from "../../lib/i18n/i18n"; |  | ||||||
| import { |  | ||||||
|   SupportedLanguage, |  | ||||||
|   getLanguageName, |  | ||||||
|   languages, |  | ||||||
| } from "../../lib/i18n/locales"; |  | ||||||
|  |  | ||||||
| export const LanguageSelector = () => { |  | ||||||
|   const [language, setLanguage] = useState<ComboboxItem>({ |  | ||||||
|     value: i18n.language, |  | ||||||
|     label: getLanguageName(i18n.language as SupportedLanguage), |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const languageOptions = Object.entries(languages).map(([code, name]) => ({ |  | ||||||
|     value: code, |  | ||||||
|     label: name, |  | ||||||
|   })); |  | ||||||
|  |  | ||||||
|   const handleLanguageChange = (option: string) => { |  | ||||||
|     i18n.changeLanguage(option as SupportedLanguage); |  | ||||||
|     setLanguage({ |  | ||||||
|       value: option, |  | ||||||
|       label: getLanguageName(option as SupportedLanguage), |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Select |  | ||||||
|       data={languageOptions} |  | ||||||
|       value={language ? language.value : null} |  | ||||||
|       onChange={(_value, option) => handleLanguageChange(option.value)} |  | ||||||
|       allowDeselect={false} |  | ||||||
|       pos="absolute" |  | ||||||
|       right={10} |  | ||||||
|       top={10} |  | ||||||
|     /> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
							
								
								
									
										35
									
								
								frontend/src/components/language/language.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | |||||||
|  | import { languages, SupportedLanguage } from "@/lib/i18n/locales"; | ||||||
|  | import { | ||||||
|  |   Select, | ||||||
|  |   SelectContent, | ||||||
|  |   SelectItem, | ||||||
|  |   SelectTrigger, | ||||||
|  |   SelectValue, | ||||||
|  | } from "../ui/select"; | ||||||
|  | import { useState } from "react"; | ||||||
|  | import i18n from "@/lib/i18n/i18n"; | ||||||
|  |  | ||||||
|  | export const LanguageSelector = () => { | ||||||
|  |   const [language, setLanguage] = useState<SupportedLanguage>( | ||||||
|  |     i18n.language as SupportedLanguage, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const handleSelect = (option: string) => { | ||||||
|  |     setLanguage(option as SupportedLanguage); | ||||||
|  |     i18n.changeLanguage(option as SupportedLanguage); | ||||||
|  |   }; | ||||||
|  |   return ( | ||||||
|  |     <Select onValueChange={handleSelect} value={language}> | ||||||
|  |       <SelectTrigger className="absolute top-5 right-5"> | ||||||
|  |         <SelectValue placeholder="Select language" /> | ||||||
|  |       </SelectTrigger> | ||||||
|  |       <SelectContent> | ||||||
|  |         {Object.entries(languages).map(([key, value]) => ( | ||||||
|  |           <SelectItem key={key} value={key}> | ||||||
|  |             {value} | ||||||
|  |           </SelectItem> | ||||||
|  |         ))} | ||||||
|  |       </SelectContent> | ||||||
|  |     </Select> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										10
									
								
								frontend/src/components/layout/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | import { LanguageSelector } from "../language/language"; | ||||||
|  |  | ||||||
|  | export const Layout = ({ children }: { children: React.ReactNode }) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex flex-col justify-center items-center min-h-svh bg-[url(/background.jpg)] bg-cover"> | ||||||
|  |       <LanguageSelector /> | ||||||
|  |       {children} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| import { Center, Flex } from "@mantine/core"; |  | ||||||
| import { ReactNode } from "react"; |  | ||||||
| import { LanguageSelector } from "../language-selector/language-selector"; |  | ||||||
|  |  | ||||||
| export const Layout = ({ children }: { children: ReactNode }) => { |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <LanguageSelector /> |  | ||||||
|       <Center style={{ minHeight: "100vh" }}> |  | ||||||
|         <Flex direction="column" flex="1" maw={340}> |  | ||||||
|           {children} |  | ||||||
|         </Flex> |  | ||||||
|       </Center> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
							
								
								
									
										61
									
								
								frontend/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,61 @@ | |||||||
|  | import * as React from "react"; | ||||||
|  | import { Slot } from "@radix-ui/react-slot"; | ||||||
|  | import { cva, type VariantProps } from "class-variance-authority"; | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils"; | ||||||
|  |  | ||||||
|  | const buttonVariants = cva( | ||||||
|  |   "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||||||
|  |   { | ||||||
|  |     variants: { | ||||||
|  |       variant: { | ||||||
|  |         default: | ||||||
|  |           "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", | ||||||
|  |         destructive: | ||||||
|  |           "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", | ||||||
|  |         outline: | ||||||
|  |           "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", | ||||||
|  |         secondary: | ||||||
|  |           "bg-secondary text-secondary-foreground shadow-xs 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", | ||||||
|  |         warning: | ||||||
|  |           "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600", | ||||||
|  |       }, | ||||||
|  |       size: { | ||||||
|  |         default: "h-9 px-4 py-2 has-[>svg]:px-3", | ||||||
|  |         sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", | ||||||
|  |         lg: "h-10 rounded-md px-6 has-[>svg]:px-4", | ||||||
|  |         icon: "size-9", | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     defaultVariants: { | ||||||
|  |       variant: "default", | ||||||
|  |       size: "default", | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | function Button({ | ||||||
|  |   className, | ||||||
|  |   variant, | ||||||
|  |   size, | ||||||
|  |   asChild = false, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<"button"> & | ||||||
|  |   VariantProps<typeof buttonVariants> & { | ||||||
|  |     asChild?: boolean; | ||||||
|  |   }) { | ||||||
|  |   const Comp = asChild ? Slot : "button"; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Comp | ||||||
|  |       data-slot="button" | ||||||
|  |       className={cn(buttonVariants({ variant, size, className }))} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Button, buttonVariants }; | ||||||
							
								
								
									
										92
									
								
								frontend/src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,92 @@ | |||||||
|  | import * as React from "react" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function Card({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card" | ||||||
|  |       className={cn( | ||||||
|  |         "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-header" | ||||||
|  |       className={cn( | ||||||
|  |         "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-title" | ||||||
|  |       className={cn("leading-none font-semibold", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-description" | ||||||
|  |       className={cn("text-muted-foreground text-sm", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardAction({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-action" | ||||||
|  |       className={cn( | ||||||
|  |         "col-start-2 row-span-2 row-start-1 self-start justify-self-end", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardContent({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-content" | ||||||
|  |       className={cn("px-6", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-footer" | ||||||
|  |       className={cn("flex items-center px-6 [.border-t]:pt-6", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |   Card, | ||||||
|  |   CardHeader, | ||||||
|  |   CardFooter, | ||||||
|  |   CardTitle, | ||||||
|  |   CardAction, | ||||||
|  |   CardDescription, | ||||||
|  |   CardContent, | ||||||
|  | } | ||||||
							
								
								
									
										75
									
								
								frontend/src/components/ui/input-otp.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,75 @@ | |||||||
|  | import * as React from "react" | ||||||
|  | import { OTPInput, OTPInputContext } from "input-otp" | ||||||
|  | import { MinusIcon } from "lucide-react" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function InputOTP({ | ||||||
|  |   className, | ||||||
|  |   containerClassName, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof OTPInput> & { | ||||||
|  |   containerClassName?: string | ||||||
|  | }) { | ||||||
|  |   return ( | ||||||
|  |     <OTPInput | ||||||
|  |       data-slot="input-otp" | ||||||
|  |       containerClassName={cn( | ||||||
|  |         "flex items-center gap-2 has-disabled:opacity-50", | ||||||
|  |         containerClassName | ||||||
|  |       )} | ||||||
|  |       className={cn("disabled:cursor-not-allowed", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="input-otp-group" | ||||||
|  |       className={cn("flex items-center", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function InputOTPSlot({ | ||||||
|  |   index, | ||||||
|  |   className, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<"div"> & { | ||||||
|  |   index: number | ||||||
|  | }) { | ||||||
|  |   const inputOTPContext = React.useContext(OTPInputContext) | ||||||
|  |   const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {} | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="input-otp-slot" | ||||||
|  |       data-active={isActive} | ||||||
|  |       className={cn( | ||||||
|  |         "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     > | ||||||
|  |       {char} | ||||||
|  |       {hasFakeCaret && ( | ||||||
|  |         <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> | ||||||
|  |           <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" /> | ||||||
|  |         </div> | ||||||
|  |       )} | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div data-slot="input-otp-separator" role="separator" {...props}> | ||||||
|  |       <MinusIcon /> | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } | ||||||
							
								
								
									
										21
									
								
								frontend/src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | import * as React from "react" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function Input({ className, type, ...props }: React.ComponentProps<"input">) { | ||||||
|  |   return ( | ||||||
|  |     <input | ||||||
|  |       type={type} | ||||||
|  |       data-slot="input" | ||||||
|  |       className={cn( | ||||||
|  |         "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", | ||||||
|  |         "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", | ||||||
|  |         "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Input } | ||||||
							
								
								
									
										22
									
								
								frontend/src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | |||||||
|  | import * as React from "react" | ||||||
|  | import * as LabelPrimitive from "@radix-ui/react-label" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function Label({ | ||||||
|  |   className, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof LabelPrimitive.Root>) { | ||||||
|  |   return ( | ||||||
|  |     <LabelPrimitive.Root | ||||||
|  |       data-slot="label" | ||||||
|  |       className={cn( | ||||||
|  |         "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Label } | ||||||
							
								
								
									
										183
									
								
								frontend/src/components/ui/select.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,183 @@ | |||||||
|  | import * as React from "react"; | ||||||
|  | import * as SelectPrimitive from "@radix-ui/react-select"; | ||||||
|  | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils"; | ||||||
|  |  | ||||||
|  | function Select({ | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Root>) { | ||||||
|  |   return <SelectPrimitive.Root data-slot="select" {...props} />; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectGroup({ | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Group>) { | ||||||
|  |   return <SelectPrimitive.Group data-slot="select-group" {...props} />; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectValue({ | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Value>) { | ||||||
|  |   return <SelectPrimitive.Value data-slot="select-value" {...props} />; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectTrigger({ | ||||||
|  |   className, | ||||||
|  |   size = "default", | ||||||
|  |   children, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { | ||||||
|  |   size?: "sm" | "default"; | ||||||
|  | }) { | ||||||
|  |   return ( | ||||||
|  |     <SelectPrimitive.Trigger | ||||||
|  |       data-slot="select-trigger" | ||||||
|  |       data-size={size} | ||||||
|  |       className={cn( | ||||||
|  |         "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-secondary dark:hover:bg-secondary/80 flex w-fit items-center justify-between gap-2 rounded-md border bg-primary px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", | ||||||
|  |         className, | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     > | ||||||
|  |       {children} | ||||||
|  |       <SelectPrimitive.Icon asChild> | ||||||
|  |         <ChevronDownIcon className="size-4" /> | ||||||
|  |       </SelectPrimitive.Icon> | ||||||
|  |     </SelectPrimitive.Trigger> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectContent({ | ||||||
|  |   className, | ||||||
|  |   children, | ||||||
|  |   position = "popper", | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Content>) { | ||||||
|  |   return ( | ||||||
|  |     <SelectPrimitive.Portal> | ||||||
|  |       <SelectPrimitive.Content | ||||||
|  |         data-slot="select-content" | ||||||
|  |         className={cn( | ||||||
|  |           "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", | ||||||
|  |           position === "popper" && | ||||||
|  |             "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", | ||||||
|  |           className, | ||||||
|  |         )} | ||||||
|  |         position={position} | ||||||
|  |         {...props} | ||||||
|  |       > | ||||||
|  |         <SelectScrollUpButton /> | ||||||
|  |         <SelectPrimitive.Viewport | ||||||
|  |           className={cn( | ||||||
|  |             "p-1", | ||||||
|  |             position === "popper" && | ||||||
|  |               "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1", | ||||||
|  |           )} | ||||||
|  |         > | ||||||
|  |           {children} | ||||||
|  |         </SelectPrimitive.Viewport> | ||||||
|  |         <SelectScrollDownButton /> | ||||||
|  |       </SelectPrimitive.Content> | ||||||
|  |     </SelectPrimitive.Portal> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectLabel({ | ||||||
|  |   className, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Label>) { | ||||||
|  |   return ( | ||||||
|  |     <SelectPrimitive.Label | ||||||
|  |       data-slot="select-label" | ||||||
|  |       className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectItem({ | ||||||
|  |   className, | ||||||
|  |   children, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Item>) { | ||||||
|  |   return ( | ||||||
|  |     <SelectPrimitive.Item | ||||||
|  |       data-slot="select-item" | ||||||
|  |       className={cn( | ||||||
|  |         "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", | ||||||
|  |         className, | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     > | ||||||
|  |       <span className="absolute right-2 flex size-3.5 items-center justify-center"> | ||||||
|  |         <SelectPrimitive.ItemIndicator> | ||||||
|  |           <CheckIcon className="size-4" /> | ||||||
|  |         </SelectPrimitive.ItemIndicator> | ||||||
|  |       </span> | ||||||
|  |       <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> | ||||||
|  |     </SelectPrimitive.Item> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectSeparator({ | ||||||
|  |   className, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.Separator>) { | ||||||
|  |   return ( | ||||||
|  |     <SelectPrimitive.Separator | ||||||
|  |       data-slot="select-separator" | ||||||
|  |       className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectScrollUpButton({ | ||||||
|  |   className, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { | ||||||
|  |   return ( | ||||||
|  |     <SelectPrimitive.ScrollUpButton | ||||||
|  |       data-slot="select-scroll-up-button" | ||||||
|  |       className={cn( | ||||||
|  |         "flex cursor-default items-center justify-center py-1", | ||||||
|  |         className, | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     > | ||||||
|  |       <ChevronUpIcon className="size-4" /> | ||||||
|  |     </SelectPrimitive.ScrollUpButton> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SelectScrollDownButton({ | ||||||
|  |   className, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { | ||||||
|  |   return ( | ||||||
|  |     <SelectPrimitive.ScrollDownButton | ||||||
|  |       data-slot="select-scroll-down-button" | ||||||
|  |       className={cn( | ||||||
|  |         "flex cursor-default items-center justify-center py-1", | ||||||
|  |         className, | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     > | ||||||
|  |       <ChevronDownIcon className="size-4" /> | ||||||
|  |     </SelectPrimitive.ScrollDownButton> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |   Select, | ||||||
|  |   SelectContent, | ||||||
|  |   SelectGroup, | ||||||
|  |   SelectItem, | ||||||
|  |   SelectLabel, | ||||||
|  |   SelectScrollDownButton, | ||||||
|  |   SelectScrollUpButton, | ||||||
|  |   SelectSeparator, | ||||||
|  |   SelectTrigger, | ||||||
|  |   SelectValue, | ||||||
|  | }; | ||||||
							
								
								
									
										28
									
								
								frontend/src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | |||||||
|  | "use client" | ||||||
|  |  | ||||||
|  | import * as React from "react" | ||||||
|  | import * as SeparatorPrimitive from "@radix-ui/react-separator" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function Separator({ | ||||||
|  |   className, | ||||||
|  |   orientation = "horizontal", | ||||||
|  |   decorative = true, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof SeparatorPrimitive.Root>) { | ||||||
|  |   return ( | ||||||
|  |     <SeparatorPrimitive.Root | ||||||
|  |       data-slot="separator-root" | ||||||
|  |       decorative={decorative} | ||||||
|  |       orientation={orientation} | ||||||
|  |       className={cn( | ||||||
|  |         "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Separator } | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| import { useSuspenseQuery } from "@tanstack/react-query"; |  | ||||||
| import React, { createContext, useContext } from "react"; |  | ||||||
| import axios from "axios"; |  | ||||||
| import { AppContextSchemaType } from "../schemas/app-context-schema"; |  | ||||||
|  |  | ||||||
| const AppContext = createContext<AppContextSchemaType | null>(null); |  | ||||||
|  |  | ||||||
| export const AppContextProvider = ({ |  | ||||||
|   children, |  | ||||||
| }: { |  | ||||||
|   children: React.ReactNode; |  | ||||||
| }) => { |  | ||||||
|   const { |  | ||||||
|     data: userContext, |  | ||||||
|     isLoading, |  | ||||||
|     error, |  | ||||||
|   } = useSuspenseQuery({ |  | ||||||
|     queryKey: ["appContext"], |  | ||||||
|     queryFn: async () => { |  | ||||||
|       const res = await axios.get("/api/app"); |  | ||||||
|       return res.data; |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (error && !isLoading) { |  | ||||||
|     throw error; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <AppContext.Provider value={userContext}>{children}</AppContext.Provider> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const useAppContext = () => { |  | ||||||
|   const context = useContext(AppContext); |  | ||||||
|  |  | ||||||
|   if (context === null) { |  | ||||||
|     throw new Error("useAppContext must be used within an AppContextProvider"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return context; |  | ||||||
| }; |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| import { useSuspenseQuery } from "@tanstack/react-query"; |  | ||||||
| import React, { createContext, useContext } from "react"; |  | ||||||
| import axios from "axios"; |  | ||||||
| import { UserContextSchemaType } from "../schemas/user-context-schema"; |  | ||||||
|  |  | ||||||
| const UserContext = createContext<UserContextSchemaType | null>(null); |  | ||||||
|  |  | ||||||
| export const UserContextProvider = ({ |  | ||||||
|   children, |  | ||||||
| }: { |  | ||||||
|   children: React.ReactNode; |  | ||||||
| }) => { |  | ||||||
|   const { |  | ||||||
|     data: userContext, |  | ||||||
|     isLoading, |  | ||||||
|     error, |  | ||||||
|   } = useSuspenseQuery({ |  | ||||||
|     queryKey: ["userContext"], |  | ||||||
|     queryFn: async () => { |  | ||||||
|       const res = await axios.get("/api/user"); |  | ||||||
|       return res.data; |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (error && !isLoading) { |  | ||||||
|     throw error; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <UserContext.Provider value={userContext}>{children}</UserContext.Provider> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const useUserContext = () => { |  | ||||||
|   const context = useContext(UserContext); |  | ||||||
|  |  | ||||||
|   if (context === null) { |  | ||||||
|     throw new Error("useUserContext must be used within a UserContextProvider"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return context; |  | ||||||
| }; |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| import type { SVGProps } from "react"; |  | ||||||
|  |  | ||||||
| export function GoogleIcon(props: SVGProps<SVGSVGElement>) { |  | ||||||
|   return ( |  | ||||||
|     <svg |  | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       width={48} |  | ||||||
|       height={48} |  | ||||||
|       viewBox="0 0 48 48" |  | ||||||
|       {...props} |  | ||||||
|     > |  | ||||||
|       <path |  | ||||||
|         fill="#ffc107" |  | ||||||
|         d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8c-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C12.955 4 4 12.955 4 24s8.955 20 20 20s20-8.955 20-20c0-1.341-.138-2.65-.389-3.917" |  | ||||||
|       ></path> |  | ||||||
|       <path |  | ||||||
|         fill="#ff3d00" |  | ||||||
|         d="m6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C16.318 4 9.656 8.337 6.306 14.691" |  | ||||||
|       ></path> |  | ||||||
|       <path |  | ||||||
|         fill="#4caf50" |  | ||||||
|         d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.9 11.9 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44" |  | ||||||
|       ></path> |  | ||||||
|       <path |  | ||||||
|         fill="#1976d2" |  | ||||||
|         d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002l6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917" |  | ||||||
|       ></path> |  | ||||||
|     </svg> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
							
								
								
									
										120
									
								
								frontend/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,120 @@ | |||||||
|  | @import "tailwindcss"; | ||||||
|  | @import "tw-animate-css"; | ||||||
|  |  | ||||||
|  | @custom-variant dark (&:is(.dark *)); | ||||||
|  |  | ||||||
|  | @theme inline { | ||||||
|  |   --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); | ||||||
|  |   --color-chart-1: var(--chart-1); | ||||||
|  |   --color-chart-2: var(--chart-2); | ||||||
|  |   --color-chart-3: var(--chart-3); | ||||||
|  |   --color-chart-4: var(--chart-4); | ||||||
|  |   --color-chart-5: var(--chart-5); | ||||||
|  |   --color-sidebar: var(--sidebar); | ||||||
|  |   --color-sidebar-foreground: var(--sidebar-foreground); | ||||||
|  |   --color-sidebar-primary: var(--sidebar-primary); | ||||||
|  |   --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); | ||||||
|  |   --color-sidebar-accent: var(--sidebar-accent); | ||||||
|  |   --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); | ||||||
|  |   --color-sidebar-border: var(--sidebar-border); | ||||||
|  |   --color-sidebar-ring: var(--sidebar-ring); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :root { | ||||||
|  |   --radius: 0.625rem; | ||||||
|  |   --background: oklch(1 0 0); | ||||||
|  |   --foreground: oklch(0.145 0 0); | ||||||
|  |   --card: oklch(1 0 0); | ||||||
|  |   --card-foreground: oklch(0.145 0 0); | ||||||
|  |   --popover: oklch(1 0 0); | ||||||
|  |   --popover-foreground: oklch(0.145 0 0); | ||||||
|  |   --primary: oklch(0.205 0 0); | ||||||
|  |   --primary-foreground: oklch(0.985 0 0); | ||||||
|  |   --secondary: oklch(0.97 0 0); | ||||||
|  |   --secondary-foreground: oklch(0.205 0 0); | ||||||
|  |   --muted: oklch(0.97 0 0); | ||||||
|  |   --muted-foreground: oklch(0.556 0 0); | ||||||
|  |   --accent: oklch(0.97 0 0); | ||||||
|  |   --accent-foreground: oklch(0.205 0 0); | ||||||
|  |   --destructive: oklch(0.577 0.245 27.325); | ||||||
|  |   --border: oklch(0.922 0 0); | ||||||
|  |   --input: oklch(0.922 0 0); | ||||||
|  |   --ring: oklch(0.708 0 0); | ||||||
|  |   --chart-1: oklch(0.646 0.222 41.116); | ||||||
|  |   --chart-2: oklch(0.6 0.118 184.704); | ||||||
|  |   --chart-3: oklch(0.398 0.07 227.392); | ||||||
|  |   --chart-4: oklch(0.828 0.189 84.429); | ||||||
|  |   --chart-5: oklch(0.769 0.188 70.08); | ||||||
|  |   --sidebar: oklch(0.985 0 0); | ||||||
|  |   --sidebar-foreground: oklch(0.145 0 0); | ||||||
|  |   --sidebar-primary: oklch(0.205 0 0); | ||||||
|  |   --sidebar-primary-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-accent: oklch(0.97 0 0); | ||||||
|  |   --sidebar-accent-foreground: oklch(0.205 0 0); | ||||||
|  |   --sidebar-border: oklch(0.922 0 0); | ||||||
|  |   --sidebar-ring: oklch(0.708 0 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark { | ||||||
|  |   --background: oklch(0.145 0 0); | ||||||
|  |   --foreground: oklch(0.985 0 0); | ||||||
|  |   --card: oklch(0.205 0 0); | ||||||
|  |   --card-foreground: oklch(0.985 0 0); | ||||||
|  |   --popover: oklch(0.205 0 0); | ||||||
|  |   --popover-foreground: oklch(0.985 0 0); | ||||||
|  |   --primary: oklch(0.922 0 0); | ||||||
|  |   --primary-foreground: oklch(0.205 0 0); | ||||||
|  |   --secondary: oklch(0.269 0 0); | ||||||
|  |   --secondary-foreground: oklch(0.985 0 0); | ||||||
|  |   --muted: oklch(0.269 0 0); | ||||||
|  |   --muted-foreground: oklch(0.708 0 0); | ||||||
|  |   --accent: oklch(0.269 0 0); | ||||||
|  |   --accent-foreground: oklch(0.985 0 0); | ||||||
|  |   --destructive: oklch(0.704 0.191 22.216); | ||||||
|  |   --border: oklch(1 0 0 / 10%); | ||||||
|  |   --input: oklch(1 0 0 / 15%); | ||||||
|  |   --ring: oklch(0.556 0 0); | ||||||
|  |   --chart-1: oklch(0.488 0.243 264.376); | ||||||
|  |   --chart-2: oklch(0.696 0.17 162.48); | ||||||
|  |   --chart-3: oklch(0.769 0.188 70.08); | ||||||
|  |   --chart-4: oklch(0.627 0.265 303.9); | ||||||
|  |   --chart-5: oklch(0.645 0.246 16.439); | ||||||
|  |   --sidebar: oklch(0.205 0 0); | ||||||
|  |   --sidebar-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-primary: oklch(0.488 0.243 264.376); | ||||||
|  |   --sidebar-primary-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-accent: oklch(0.269 0 0); | ||||||
|  |   --sidebar-accent-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-border: oklch(1 0 0 / 10%); | ||||||
|  |   --sidebar-ring: oklch(0.556 0 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @layer base { | ||||||
|  |   * { | ||||||
|  |     @apply border-border outline-ring/50; | ||||||
|  |   } | ||||||
|  |   body { | ||||||
|  |     @apply bg-background text-foreground; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,28 +1,14 @@ | |||||||
| import i18n from "i18next"; | import i18n from "i18next"; | ||||||
| import { initReactI18next } from "react-i18next"; | import { initReactI18next } from "react-i18next"; | ||||||
| import LanguageDetector from "i18next-browser-languagedetector"; | import LanguageDetector from "i18next-browser-languagedetector"; | ||||||
| import ChainedBackend from "i18next-chained-backend"; |  | ||||||
| import resourcesToBackend from "i18next-resources-to-backend"; | import resourcesToBackend from "i18next-resources-to-backend"; | ||||||
| import HttpBackend from "i18next-http-backend"; |  | ||||||
|  |  | ||||||
| const backends = [ |  | ||||||
|   HttpBackend, |  | ||||||
|   resourcesToBackend( |  | ||||||
|     (language: string) => import(`./locales/${language}.json`), |  | ||||||
|   ), |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| const backendOptions =  [ |  | ||||||
|   { |  | ||||||
|     loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json", |  | ||||||
|   }, |  | ||||||
|   {} |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| i18n | i18n | ||||||
|   .use(ChainedBackend) |  | ||||||
|   .use(LanguageDetector) |   .use(LanguageDetector) | ||||||
|   .use(initReactI18next) |   .use(initReactI18next) | ||||||
|  |   .use(resourcesToBackend( | ||||||
|  |     (language: string) => import(`./locales/${language}.json`), | ||||||
|  |   )) | ||||||
|   .init({ |   .init({ | ||||||
|     fallbackLng: "en", |     fallbackLng: "en", | ||||||
|     debug: import.meta.env.MODE === "development", |     debug: import.meta.env.MODE === "development", | ||||||
| @@ -32,11 +18,6 @@ i18n | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     load: "currentOnly", |     load: "currentOnly", | ||||||
|  |  | ||||||
|     backend: { |  | ||||||
|       backends: import.meta.env.MODE !== "development" ? backends : backends.reverse(), |  | ||||||
|       backendOptions: import.meta.env.MODE !== "development" ? backendOptions : backendOptions.reverse() |  | ||||||
|     }, |  | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| export default i18n; | export default i18n; | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Welcome back, login with", |     "loginTitle": "Welcome back, login with", | ||||||
|  |     "loginTitleSimple": "Welcome back, please login", | ||||||
|     "loginDivider": "Or continue with password", |     "loginDivider": "Or continue with password", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "Username", | ||||||
|     "loginPassword": "Password", |     "loginPassword": "Password", | ||||||
| @@ -18,7 +19,7 @@ | |||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueInvalidRedirectTitle": "Invalid redirect", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code>, are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueTitle": "Continue", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueSubtitle": "Click the button to continue to your app.", | ||||||
|     "internalErrorTitle": "Internal Server Error", |     "internalErrorTitle": "Internal Server Error", | ||||||
| @@ -29,8 +30,8 @@ | |||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
|     "logoutSuccessSubtitle": "You have been logged out", |     "logoutSuccessSubtitle": "You have been logged out", | ||||||
|     "logoutTitle": "Logout", |     "logoutTitle": "Logout", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.", |     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>, click the button below to logout.", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.", |     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider, click the button below to logout.", | ||||||
|     "notFoundTitle": "Page not found", |     "notFoundTitle": "Page not found", | ||||||
|     "notFoundSubtitle": "The page you are looking for does not exist.", |     "notFoundSubtitle": "The page you are looking for does not exist.", | ||||||
|     "notFoundButton": "Go home", |     "notFoundButton": "Go home", | ||||||
| @@ -38,13 +39,18 @@ | |||||||
|     "totpFailSubtitle": "Please check your code and try again", |     "totpFailSubtitle": "Please check your code and try again", | ||||||
|     "totpSuccessTitle": "Verified", |     "totpSuccessTitle": "Verified", | ||||||
|     "totpSuccessSubtitle": "Redirecting to your app", |     "totpSuccessSubtitle": "Redirecting to your app", | ||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "TOTP Verification", | ||||||
|  |     "totpSubtitle": "Please enter the code from your authenticator app.", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unaothorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?" |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|  |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|  |     "errorTitle": "An error occurred", | ||||||
|  |     "errorSubtitle": "An error occured while trying to perform this action. Please check the console for more information." | ||||||
| } | } | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| { | { | ||||||
|     "loginTitle": "Welcome back, login with", |     "loginTitle": "Welcome back, login with", | ||||||
|  |     "loginTitleSimple": "Welcome back, please login", | ||||||
|     "loginDivider": "Or continue with password", |     "loginDivider": "Or continue with password", | ||||||
|     "loginUsername": "Username", |     "loginUsername": "Username", | ||||||
|     "loginPassword": "Password", |     "loginPassword": "Password", | ||||||
| @@ -18,7 +19,7 @@ | |||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueInvalidRedirectTitle": "Invalid redirect", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code>, are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueTitle": "Continue", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueSubtitle": "Click the button to continue to your app.", | ||||||
|     "internalErrorTitle": "Internal Server Error", |     "internalErrorTitle": "Internal Server Error", | ||||||
| @@ -29,8 +30,8 @@ | |||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
|     "logoutSuccessSubtitle": "You have been logged out", |     "logoutSuccessSubtitle": "You have been logged out", | ||||||
|     "logoutTitle": "Logout", |     "logoutTitle": "Logout", | ||||||
|     "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.", |     "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>, click the button below to logout.", | ||||||
|     "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.", |     "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider, click the button below to logout.", | ||||||
|     "notFoundTitle": "Page not found", |     "notFoundTitle": "Page not found", | ||||||
|     "notFoundSubtitle": "The page you are looking for does not exist.", |     "notFoundSubtitle": "The page you are looking for does not exist.", | ||||||
|     "notFoundButton": "Go home", |     "notFoundButton": "Go home", | ||||||
| @@ -39,12 +40,17 @@ | |||||||
|     "totpSuccessTitle": "Verified", |     "totpSuccessTitle": "Verified", | ||||||
|     "totpSuccessSubtitle": "Redirecting to your app", |     "totpSuccessSubtitle": "Redirecting to your app", | ||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|  |     "totpSubtitle": "Please enter the code from your authenticator app.", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unaothorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?" |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|  |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
|  |     "errorTitle": "An error occurred", | ||||||
|  |     "errorSubtitle": "An error occured while trying to perform this action. Please check the console for more information." | ||||||
| } | } | ||||||
							
								
								
									
										15
									
								
								frontend/src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | |||||||
|  | import { clsx, type ClassValue } from "clsx" | ||||||
|  | import { twMerge } from "tailwind-merge" | ||||||
|  |  | ||||||
|  | export function cn(...inputs: ClassValue[]) { | ||||||
|  |   return twMerge(clsx(inputs)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const isValidUrl = (url: string) => { | ||||||
|  |   try { | ||||||
|  |     new URL(url) | ||||||
|  |     return true | ||||||
|  |   } catch (e) { | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,50 +1,47 @@ | |||||||
| import { StrictMode } from "react"; | import { StrictMode } from "react"; | ||||||
| import { createRoot } from "react-dom/client"; | import { createRoot } from "react-dom/client"; | ||||||
| import { App } from "./App.tsx"; | import "./index.css"; | ||||||
| import { MantineProvider } from "@mantine/core"; | import { Layout } from "./components/layout/layout.tsx"; | ||||||
| import { Notifications } from "@mantine/notifications"; | import { createBrowserRouter, RouterProvider } from "react-router"; | ||||||
| import "@mantine/core/styles.css"; |  | ||||||
| import "@mantine/notifications/styles.css"; |  | ||||||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |  | ||||||
| import { BrowserRouter, Route } from "react-router"; |  | ||||||
| import { Routes } from "react-router"; |  | ||||||
| import { UserContextProvider } from "./context/user-context.tsx"; |  | ||||||
| import { LoginPage } from "./pages/login-page.tsx"; | import { LoginPage } from "./pages/login-page.tsx"; | ||||||
| import { LogoutPage } from "./pages/logout-page.tsx"; | import { App } from "./App.tsx"; | ||||||
| import { ContinuePage } from "./pages/continue-page.tsx"; | import { ErrorPage } from "./pages/error-page.tsx"; | ||||||
| import { NotFoundPage } from "./pages/not-found-page.tsx"; | import { NotFoundPage } from "./pages/not-found-page.tsx"; | ||||||
| import { UnauthorizedPage } from "./pages/unauthorized-page.tsx"; | import { ContinuePage } from "./pages/continue-page.tsx"; | ||||||
| import { InternalServerError } from "./pages/internal-server-error.tsx"; |  | ||||||
| import { TotpPage } from "./pages/totp-page.tsx"; | import { TotpPage } from "./pages/totp-page.tsx"; | ||||||
| import { AppContextProvider } from "./context/app-context.tsx"; |  | ||||||
| import "./lib/i18n/i18n.ts"; |  | ||||||
| import { ForgotPasswordPage } from "./pages/forgot-password-page.tsx"; |  | ||||||
|  |  | ||||||
| const queryClient = new QueryClient(); | const router = createBrowserRouter([ | ||||||
|  |   { | ||||||
|  |     path: "/", | ||||||
|  |     element: <App />, | ||||||
|  |     errorElement: <ErrorPage />, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/login", | ||||||
|  |     element: <LoginPage />, | ||||||
|  |     errorElement: <ErrorPage />, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/continue", | ||||||
|  |     element: <ContinuePage />, | ||||||
|  |     errorElement: <ErrorPage />, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/totp", | ||||||
|  |     element: <TotpPage />, | ||||||
|  |     errorElement: <ErrorPage />, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "*", | ||||||
|  |     element: <NotFoundPage />, | ||||||
|  |     errorElement: <ErrorPage />, | ||||||
|  |   }, | ||||||
|  | ]); | ||||||
|  |  | ||||||
| createRoot(document.getElementById("root")!).render( | createRoot(document.getElementById("root")!).render( | ||||||
|   <StrictMode> |   <StrictMode> | ||||||
|     <MantineProvider defaultColorScheme="auto"> |     <Layout> | ||||||
|       <QueryClientProvider client={queryClient}> |       <RouterProvider router={router} /> | ||||||
|         <Notifications /> |     </Layout> | ||||||
|         <AppContextProvider> |  | ||||||
|           <UserContextProvider> |  | ||||||
|             <BrowserRouter> |  | ||||||
|               <Routes> |  | ||||||
|                 <Route path="/" element={<App />} /> |  | ||||||
|                 <Route path="/login" element={<LoginPage />} /> |  | ||||||
|                 <Route path="/totp" element={<TotpPage />} /> |  | ||||||
|                 <Route path="/logout" element={<LogoutPage />} /> |  | ||||||
|                 <Route path="/continue" element={<ContinuePage />} /> |  | ||||||
|                 <Route path="/unauthorized" element={<UnauthorizedPage />} /> |  | ||||||
|                 <Route path="/error" element={<InternalServerError />} /> |  | ||||||
|                 <Route path="/forgot-password" element={<ForgotPasswordPage />} /> |  | ||||||
|                 <Route path="*" element={<NotFoundPage />} /> |  | ||||||
|               </Routes> |  | ||||||
|             </BrowserRouter> |  | ||||||
|           </UserContextProvider> |  | ||||||
|         </AppContextProvider> |  | ||||||
|       </QueryClientProvider> |  | ||||||
|     </MantineProvider> |  | ||||||
|   </StrictMode>, |   </StrictMode>, | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -1,134 +1,120 @@ | |||||||
| import { Button, Code, Paper, Text } from "@mantine/core"; | import { Button } from "@/components/ui/button"; | ||||||
| import { notifications } from "@mantine/notifications"; | import { | ||||||
| import { Navigate } from "react-router"; |   Card, | ||||||
| import { useUserContext } from "../context/user-context"; |   CardContent, | ||||||
| import { Layout } from "../components/layouts/layout"; |   CardDescription, | ||||||
| import { ReactNode } from "react"; |   CardHeader, | ||||||
| import { escapeRegex, isQueryValid } from "../utils/utils"; |   CardTitle, | ||||||
| import { useAppContext } from "../context/app-context"; | } from "@/components/ui/card"; | ||||||
|  | import { isValidUrl } from "@/lib/utils"; | ||||||
| import { Trans, useTranslation } from "react-i18next"; | import { Trans, useTranslation } from "react-i18next"; | ||||||
|  | import { Navigate, useNavigate } from "react-router"; | ||||||
|  |  | ||||||
| export const ContinuePage = () => { | export const ContinuePage = () => { | ||||||
|   const queryString = window.location.search; |  | ||||||
|   const params = new URLSearchParams(queryString); |  | ||||||
|   const redirectUri = params.get("redirect_uri") ?? ""; |  | ||||||
|  |  | ||||||
|   const { isLoggedIn } = useUserContext(); |  | ||||||
|   const { disableContinue, domain } = useAppContext(); |  | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |   const params = new URLSearchParams(window.location.search); | ||||||
|  |  | ||||||
|   if (!isLoggedIn) { |   const redirectURI = params.get("redirect_uri") ?? ""; | ||||||
|     return <Navigate to={`/login?redirect_uri=${redirectUri}`} />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (!isQueryValid(redirectUri)) { |   //psuedo | ||||||
|  |   const domain = "127.0.0.1"; | ||||||
|  |   const disableContinue = false; | ||||||
|  |  | ||||||
|  |   if (redirectURI === "") { | ||||||
|     return <Navigate to="/" />; |     return <Navigate to="/" />; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const redirect = () => { |   if (!isValidUrl(redirectURI)) { | ||||||
|     notifications.show({ |     return <Navigate to="/" />; | ||||||
|       title: t("continueRedirectingTitle"), |  | ||||||
|       message: t("continueRedirectingSubtitle"), |  | ||||||
|       color: "blue", |  | ||||||
|     }); |  | ||||||
|     setTimeout(() => { |  | ||||||
|       window.location.href = redirectUri; |  | ||||||
|     }, 500); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   let uri; |  | ||||||
|  |  | ||||||
|   try { |  | ||||||
|     uri = new URL(redirectUri); |  | ||||||
|   } catch { |  | ||||||
|     return ( |  | ||||||
|       <ContinuePageLayout> |  | ||||||
|         <Text size="xl" fw={700}> |  | ||||||
|           {t("Invalid redirect")} |  | ||||||
|         </Text> |  | ||||||
|         <Text>{t("The redirect URL is invalid")}</Text> |  | ||||||
|       </ContinuePageLayout> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const regex = new RegExp(`^.*${escapeRegex(domain)}$`) |  | ||||||
|  |  | ||||||
|   if (!regex.test(uri.hostname)) { |  | ||||||
|     return ( |  | ||||||
|       <ContinuePageLayout> |  | ||||||
|         <Text size="xl" fw={700}> |  | ||||||
|           {t("untrustedRedirectTitle")} |  | ||||||
|         </Text> |  | ||||||
|         <Trans |  | ||||||
|             i18nKey="untrustedRedirectSubtitle" |  | ||||||
|             t={t} |  | ||||||
|             components={{ Code: <Code /> }} |  | ||||||
|             values={{ domain: domain }} |  | ||||||
|           /> |  | ||||||
|         <Button fullWidth mt="xl" color="red" onClick={redirect}> |  | ||||||
|           {t('continueTitle')} |  | ||||||
|         </Button> |  | ||||||
|         <Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}> |  | ||||||
|           {t('cancelTitle')} |  | ||||||
|         </Button> |  | ||||||
|       </ContinuePageLayout> |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (disableContinue) { |   if (disableContinue) { | ||||||
|     window.location.href = redirectUri; |     window.location.href = redirectURI; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const url = new URL(redirectURI); | ||||||
|  |  | ||||||
|  |   if (!url.hostname.includes(domain)) { | ||||||
|     return ( |     return ( | ||||||
|       <ContinuePageLayout> |       <Card className="min-w-xs md:max-w-sm"> | ||||||
|         <Text size="xl" fw={700}> |         <CardHeader> | ||||||
|           {t("continueRedirectingTitle")} |           <CardTitle className="text-3xl"> | ||||||
|         </Text> |             {t("untrustedRedirectTitle")} | ||||||
|         <Text>{t("continueRedirectingSubtitle")}</Text> |           </CardTitle> | ||||||
|       </ContinuePageLayout> |           <CardDescription> | ||||||
|  |             <Trans | ||||||
|  |               i18nKey="untrustedRedirectSubtitle" | ||||||
|  |               t={t} | ||||||
|  |               components={{ | ||||||
|  |                 code: ( | ||||||
|  |                   <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold" /> | ||||||
|  |                 ), | ||||||
|  |               }} | ||||||
|  |               values={{ domain }} | ||||||
|  |             /> | ||||||
|  |           </CardDescription> | ||||||
|  |         </CardHeader> | ||||||
|  |         <CardContent className="flex flex-col gap-2 items-stretch"> | ||||||
|  |           <Button | ||||||
|  |             onClick={() => window.location.replace(redirectURI)} | ||||||
|  |             variant="destructive" | ||||||
|  |           > | ||||||
|  |             {t("continueTitle")} | ||||||
|  |           </Button> | ||||||
|  |           <Button onClick={() => navigate("/")} variant="outline"> | ||||||
|  |             {t("cancelTitle")} | ||||||
|  |           </Button> | ||||||
|  |         </CardContent> | ||||||
|  |       </Card> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (window.location.protocol === "https:" && uri.protocol === "http:") { |   if (url.protocol === "http:" && window.location.protocol === "https:") { | ||||||
|     return ( |     return ( | ||||||
|       <ContinuePageLayout> |       <Card className="min-w-xs md:max-w-sm"> | ||||||
|         <Text size="xl" fw={700}> |         <CardHeader> | ||||||
|           {t("continueInsecureRedirectTitle")} |           <CardTitle className="text-3xl"> | ||||||
|         </Text> |             {t("continueInsecureRedirectTitle")} | ||||||
|         <Text> |           </CardTitle> | ||||||
|           <Trans |           <CardDescription> | ||||||
|             i18nKey="continueInsecureRedirectSubtitle" |             <Trans | ||||||
|             t={t} |               i18nKey="continueInsecureRedirectSubtitle" | ||||||
|             components={{ Code: <Code /> }} |               t={t} | ||||||
|           /> |               components={{ | ||||||
|         </Text> |                 code: ( | ||||||
|         <Button fullWidth mt="xl" color="yellow" onClick={redirect}> |                   <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold" /> | ||||||
|  |                 ), | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |           </CardDescription> | ||||||
|  |         </CardHeader> | ||||||
|  |         <CardContent className="flex flex-col gap-2 items-stretch"> | ||||||
|  |           <Button | ||||||
|  |             onClick={() => window.location.replace(redirectURI)} | ||||||
|  |             variant="warning" | ||||||
|  |           > | ||||||
|  |             {t("continueTitle")} | ||||||
|  |           </Button> | ||||||
|  |           <Button onClick={() => navigate("/")} variant="outline"> | ||||||
|  |             {t("cancelTitle")} | ||||||
|  |           </Button> | ||||||
|  |         </CardContent> | ||||||
|  |       </Card> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Card className="min-w-xs md:max-w-sm"> | ||||||
|  |       <CardHeader> | ||||||
|  |         <CardTitle className="text-3xl">{t("continueTitle")}</CardTitle> | ||||||
|  |         <CardDescription>{t("continueSubtitle")}</CardDescription> | ||||||
|  |       </CardHeader> | ||||||
|  |       <CardContent className="flex flex-col items-stretch"> | ||||||
|  |         <Button onClick={() => window.location.replace(redirectURI)}> | ||||||
|           {t("continueTitle")} |           {t("continueTitle")} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}> |       </CardContent> | ||||||
|           {t('cancelTitle')} |     </Card> | ||||||
|         </Button> |  | ||||||
|       </ContinuePageLayout> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <ContinuePageLayout> |  | ||||||
|       <Text size="xl" fw={700}> |  | ||||||
|         {t("continueTitle")} |  | ||||||
|       </Text> |  | ||||||
|       <Text>{t("continueSubtitle")}</Text> |  | ||||||
|       <Button fullWidth mt="xl" onClick={redirect}> |  | ||||||
|         {t("continueTitle")} |  | ||||||
|       </Button> |  | ||||||
|     </ContinuePageLayout> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const ContinuePageLayout = ({ children }: { children: ReactNode }) => { |  | ||||||
|   return ( |  | ||||||
|     <Layout> |  | ||||||
|       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> |  | ||||||
|         {children} |  | ||||||
|       </Paper> |  | ||||||
|     </Layout> |  | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								frontend/src/pages/error-page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | |||||||
|  | import { | ||||||
|  |   Card, | ||||||
|  |   CardDescription, | ||||||
|  |   CardHeader, | ||||||
|  |   CardTitle, | ||||||
|  | } from "@/components/ui/card"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
|  |  | ||||||
|  | export const ErrorPage = () => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Card className="min-w-xs md:max-w-sm"> | ||||||
|  |       <CardHeader> | ||||||
|  |         <CardTitle className="text-3xl">{t("errorTitle")}</CardTitle> | ||||||
|  |         <CardDescription>{t("errorSubtitle")}</CardDescription> | ||||||
|  |       </CardHeader> | ||||||
|  |     </Card> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| import { Paper, Text, TypographyStylesProvider } from "@mantine/core"; |  | ||||||
| import { Layout } from "../components/layouts/layout"; |  | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
| import { useAppContext } from "../context/app-context"; |  | ||||||
| import Markdown from 'react-markdown' |  | ||||||
|  |  | ||||||
| export const ForgotPasswordPage = () => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const { forgotPasswordMessage } = useAppContext(); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Layout> |  | ||||||
|       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> |  | ||||||
|         <Text size="xl" fw={700}> |  | ||||||
|           {t("forgotPasswordTitle")} |  | ||||||
|         </Text> |  | ||||||
|         <TypographyStylesProvider> |  | ||||||
|             <Markdown> |  | ||||||
|                 {forgotPasswordMessage} |  | ||||||
|             </Markdown> |  | ||||||
|         </TypographyStylesProvider> |  | ||||||
|       </Paper> |  | ||||||
|     </Layout> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| import { Button, Paper, Text } from "@mantine/core"; |  | ||||||
| import { Layout } from "../components/layouts/layout"; |  | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
|  |  | ||||||
| export const InternalServerError = () => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   return ( |  | ||||||
|     <Layout> |  | ||||||
|       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> |  | ||||||
|         <Text size="xl" fw={700}> |  | ||||||
|           {t("internalErrorTitle")} |  | ||||||
|         </Text> |  | ||||||
|         <Text>{t("internalErrorSubtitle")}</Text> |  | ||||||
|         <Button fullWidth mt="xl" onClick={() => window.location.replace("/")}> |  | ||||||
|           {t("internalErrorButton")} |  | ||||||
|         </Button> |  | ||||||
|       </Paper> |  | ||||||
|     </Layout> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,138 +1,100 @@ | |||||||
| import { Paper, Title, Text, Divider } from "@mantine/core"; | import { OAuthButton } from "@/components/auth/oauth-button"; | ||||||
| import { notifications } from "@mantine/notifications"; | import { GenericIcon } from "@/components/icons/generic"; | ||||||
| import { useMutation } from "@tanstack/react-query"; | import { GithubIcon } from "@/components/icons/github"; | ||||||
| import axios, { type AxiosError } from "axios"; | import { GoogleIcon } from "@/components/icons/google"; | ||||||
| import { useUserContext } from "../context/user-context"; | import { Button } from "@/components/ui/button"; | ||||||
| import { Navigate } from "react-router"; | import { | ||||||
| import { Layout } from "../components/layouts/layout"; |   Card, | ||||||
| import { OAuthButtons } from "../components/auth/oauth-buttons"; |   CardHeader, | ||||||
| import { LoginFormValues } from "../schemas/login-schema"; |   CardTitle, | ||||||
| import { LoginForm } from "../components/auth/login-forn"; |   CardDescription, | ||||||
| import { isQueryValid } from "../utils/utils"; |   CardContent, | ||||||
| import { useAppContext } from "../context/app-context"; | } from "@/components/ui/card"; | ||||||
|  | import { Input } from "@/components/ui/input"; | ||||||
|  | import { Label } from "@/components/ui/label"; | ||||||
|  | import { Separator } from "@/components/ui/separator"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
|  |  | ||||||
| export const LoginPage = () => { | export const LoginPage = () => { | ||||||
|   const queryString = window.location.search; |  | ||||||
|   const params = new URLSearchParams(queryString); |  | ||||||
|   const redirectUri = params.get("redirect_uri") ?? ""; |  | ||||||
|  |  | ||||||
|   const { isLoggedIn } = useUserContext(); |  | ||||||
|   const { configuredProviders, title, genericName } = useAppContext(); |  | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const configuredProviders = ["google", "github", "generic", "username"]; | ||||||
|  |   const title = "Tinyauth"; | ||||||
|  |  | ||||||
|   const oauthProviders = configuredProviders.filter( |   const oauthConfigured = | ||||||
|     (value) => value !== "username", |     configuredProviders.filter((provider) => provider !== "username").length > | ||||||
|   ); |     0; | ||||||
|  |   const userAuthConfigured = configuredProviders.includes("username"); | ||||||
|   if (isLoggedIn) { |  | ||||||
|     return <Navigate to="/logout" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const loginMutation = useMutation({ |  | ||||||
|     mutationFn: (login: LoginFormValues) => { |  | ||||||
|       return axios.post("/api/login", login); |  | ||||||
|     }, |  | ||||||
|     onError: (data: AxiosError) => { |  | ||||||
|       if (data.response) { |  | ||||||
|         if (data.response.status === 429) { |  | ||||||
|           notifications.show({ |  | ||||||
|             title: t("loginFailTitle"), |  | ||||||
|             message: t("loginFailRateLimit"), |  | ||||||
|             color: "red", |  | ||||||
|           }); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("loginFailTitle"), |  | ||||||
|         message: t("loginFailSubtitle"), |  | ||||||
|         color: "red", |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     onSuccess: async (data) => { |  | ||||||
|       if (data.data.totpPending) { |  | ||||||
|         window.location.replace(`/totp?redirect_uri=${redirectUri}`); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("loginSuccessTitle"), |  | ||||||
|         message: t("loginSuccessSubtitle"), |  | ||||||
|         color: "green", |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       setTimeout(() => { |  | ||||||
|         if (!isQueryValid(redirectUri)) { |  | ||||||
|           window.location.replace("/"); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         window.location.replace(`/continue?redirect_uri=${redirectUri}`); |  | ||||||
|       }, 500); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const loginOAuthMutation = useMutation({ |  | ||||||
|     mutationFn: (provider: string) => { |  | ||||||
|       return axios.get( |  | ||||||
|         `/api/oauth/url/${provider}?redirect_uri=${redirectUri}`, |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     onError: () => { |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("loginOauthFailTitle"), |  | ||||||
|         message: t("loginOauthFailSubtitle"), |  | ||||||
|         color: "red", |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     onSuccess: (data) => { |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("loginOauthSuccessTitle"), |  | ||||||
|         message: t("loginOauthSuccessSubtitle"), |  | ||||||
|         color: "blue", |  | ||||||
|       }); |  | ||||||
|       setTimeout(() => { |  | ||||||
|         window.location.href = data.data.url; |  | ||||||
|       }, 500); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const handleSubmit = (values: LoginFormValues) => { |  | ||||||
|     loginMutation.mutate(values); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Layout> |     <Card className="max-w-xs md:max-w-sm"> | ||||||
|       <Title ta="center">{title}</Title> |       <CardHeader> | ||||||
|       <Paper shadow="md" p="xl" mt={30} radius="md" withBorder> |         <CardTitle className="text-center text-3xl">{title}</CardTitle> | ||||||
|         {oauthProviders.length > 0 && ( |         {configuredProviders.length > 0 && ( | ||||||
|           <> |           <CardDescription className="text-center"> | ||||||
|             <Text size="lg" fw={500} ta="center"> |             {oauthConfigured ? t("loginTitle") : t("loginTitleSimple")} | ||||||
|               {t("loginTitle")} |           </CardDescription> | ||||||
|             </Text> |         )} | ||||||
|             <OAuthButtons |       </CardHeader> | ||||||
|               oauthProviders={oauthProviders} |       <CardContent className="flex flex-col gap-4"> | ||||||
|               isPending={loginOAuthMutation.isPending} |         {oauthConfigured && ( | ||||||
|               mutate={loginOAuthMutation.mutate} |           <div className="flex flex-row gap-3 flex-wrap items-center justify-center"> | ||||||
|               genericName={genericName} |             {configuredProviders.includes("google") && ( | ||||||
|             /> |               <OAuthButton title="Google" icon={<GoogleIcon />} /> | ||||||
|             {configuredProviders.includes("username") && ( |  | ||||||
|               <Divider |  | ||||||
|                 label={t("loginDivider")} |  | ||||||
|                 labelPosition="center" |  | ||||||
|                 my="lg" |  | ||||||
|               /> |  | ||||||
|             )} |             )} | ||||||
|           </> |             {configuredProviders.includes("github") && ( | ||||||
|  |               <OAuthButton title="Github" icon={<GithubIcon />} /> | ||||||
|  |             )} | ||||||
|  |             {configuredProviders.includes("generic") && ( | ||||||
|  |               <OAuthButton title="Generic" icon={<GenericIcon />} /> | ||||||
|  |             )} | ||||||
|  |           </div> | ||||||
|         )} |         )} | ||||||
|         {configuredProviders.includes("username") && ( |         {userAuthConfigured && oauthConfigured && ( | ||||||
|           <LoginForm |           <div className="flex items-center gap-4"> | ||||||
|             isPending={loginMutation.isPending} |             <Separator className="flex-1" /> | ||||||
|             onSubmit={handleSubmit} |             <span className="text-sm text-muted-foreground"> | ||||||
|           /> |               {t("loginDivider")} | ||||||
|  |             </span> | ||||||
|  |             <Separator className="flex-1" /> | ||||||
|  |           </div> | ||||||
|         )} |         )} | ||||||
|       </Paper> |         {userAuthConfigured && ( | ||||||
|     </Layout> |           <div className="flex flex-col gap-4"> | ||||||
|  |             <div> | ||||||
|  |               <Label htmlFor="#username">{t("loginUsername")}</Label> | ||||||
|  |               <Input | ||||||
|  |                 id="username" | ||||||
|  |                 placeholder={t("loginUsername")} | ||||||
|  |                 className="mt-2" | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|  |             <div> | ||||||
|  |               <Label htmlFor="#password"> | ||||||
|  |                 <div className="flex flex-row min-w-full items-center justify-between"> | ||||||
|  |                   <span>{t("loginPassword")}</span> | ||||||
|  |                   <a | ||||||
|  |                     href="/forgot" | ||||||
|  |                     className="text-muted-foreground font-normal" | ||||||
|  |                   > | ||||||
|  |                     {t("forgotPasswordTitle")} | ||||||
|  |                   </a> | ||||||
|  |                 </div> | ||||||
|  |               </Label> | ||||||
|  |               <Input | ||||||
|  |                 id="password" | ||||||
|  |                 placeholder={t("loginPassword")} | ||||||
|  |                 className="mt-2" | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|  |             <Button>{t("loginSubmit")}</Button> | ||||||
|  |           </div> | ||||||
|  |         )} | ||||||
|  |         {configuredProviders.length == 0 && ( | ||||||
|  |           <h3 className="text-center text-xl text-red-600"> | ||||||
|  |             {t("failedToFetchProvidersTitle")} | ||||||
|  |           </h3> | ||||||
|  |         )} | ||||||
|  |       </CardContent> | ||||||
|  |     </Card> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,84 +0,0 @@ | |||||||
| import { Button, Code, Paper, Text } from "@mantine/core"; |  | ||||||
| import { notifications } from "@mantine/notifications"; |  | ||||||
| import { useMutation } from "@tanstack/react-query"; |  | ||||||
| import axios from "axios"; |  | ||||||
| import { useUserContext } from "../context/user-context"; |  | ||||||
| import { Navigate } from "react-router"; |  | ||||||
| import { Layout } from "../components/layouts/layout"; |  | ||||||
| import { capitalize } from "../utils/utils"; |  | ||||||
| import { useAppContext } from "../context/app-context"; |  | ||||||
| import { Trans, useTranslation } from "react-i18next"; |  | ||||||
|  |  | ||||||
| export const LogoutPage = () => { |  | ||||||
|   const { isLoggedIn, username, oauth, provider } = useUserContext(); |  | ||||||
|   const { genericName } = useAppContext(); |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|  |  | ||||||
|   if (!isLoggedIn) { |  | ||||||
|     return <Navigate to="/login" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const logoutMutation = useMutation({ |  | ||||||
|     mutationFn: () => { |  | ||||||
|       return axios.post("/api/logout"); |  | ||||||
|     }, |  | ||||||
|     onError: () => { |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("logoutFailTitle"), |  | ||||||
|         message: t("logoutFailSubtitle"), |  | ||||||
|         color: "red", |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     onSuccess: () => { |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("logoutSuccessTitle"), |  | ||||||
|         message: t("logoutSuccessSubtitle"), |  | ||||||
|         color: "green", |  | ||||||
|       }); |  | ||||||
|       setTimeout(() => { |  | ||||||
|         window.location.replace("/login"); |  | ||||||
|       }, 500); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Layout> |  | ||||||
|       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> |  | ||||||
|         <Text size="xl" fw={700}> |  | ||||||
|           {t("logoutTitle")} |  | ||||||
|         </Text> |  | ||||||
|         <Text> |  | ||||||
|           {oauth ? ( |  | ||||||
|             <Trans |  | ||||||
|               i18nKey="logoutOauthSubtitle" |  | ||||||
|               t={t} |  | ||||||
|               components={{ Code: <Code /> }} |  | ||||||
|               values={{ |  | ||||||
|                 provider: |  | ||||||
|                   provider === "generic" ? genericName : capitalize(provider), |  | ||||||
|                 username: username, |  | ||||||
|               }} |  | ||||||
|             /> |  | ||||||
|           ) : ( |  | ||||||
|             <Trans |  | ||||||
|               i18nKey="logoutUsernameSubtitle" |  | ||||||
|               t={t} |  | ||||||
|               components={{ Code: <Code /> }} |  | ||||||
|               values={{ |  | ||||||
|                 username: username, |  | ||||||
|               }} |  | ||||||
|             /> |  | ||||||
|           )} |  | ||||||
|         </Text> |  | ||||||
|         <Button |  | ||||||
|           fullWidth |  | ||||||
|           mt="xl" |  | ||||||
|           onClick={() => logoutMutation.mutate()} |  | ||||||
|           loading={logoutMutation.isPending} |  | ||||||
|         > |  | ||||||
|           {t("logoutTitle")} |  | ||||||
|         </Button> |  | ||||||
|       </Paper> |  | ||||||
|     </Layout> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,20 +1,27 @@ | |||||||
| import { Button, Paper, Text } from "@mantine/core"; | import { Button } from "@/components/ui/button"; | ||||||
| import { Layout } from "../components/layouts/layout"; | import { | ||||||
|  |   Card, | ||||||
|  |   CardContent, | ||||||
|  |   CardDescription, | ||||||
|  |   CardHeader, | ||||||
|  |   CardTitle, | ||||||
|  | } from "@/components/ui/card"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
|  | import { useNavigate } from "react-router"; | ||||||
|  |  | ||||||
| export const NotFoundPage = () => { | export const NotFoundPage = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Layout> |     <Card className="min-w-xs md:max-w-sm"> | ||||||
|       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> |       <CardHeader> | ||||||
|         <Text size="xl" fw={700}> |         <CardTitle className="text-3xl">{t("notFoundTitle")}</CardTitle> | ||||||
|           {t("notFoundTitle")} |         <CardDescription>{t("notFoundSubtitle")}</CardDescription> | ||||||
|         </Text> |       </CardHeader> | ||||||
|         <Text>{t("notFoundSubtitle")}</Text> |       <CardContent className="flex flex-col items-stretch"> | ||||||
|         <Button fullWidth mt="xl" onClick={() => window.location.replace("/")}> |         <Button onClick={() => navigate("/")}>{t("notFoundButton")}</Button> | ||||||
|           {t("notFoundButton")} |       </CardContent> | ||||||
|         </Button> |     </Card> | ||||||
|       </Paper> |  | ||||||
|     </Layout> |  | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,66 +1,44 @@ | |||||||
| import { Navigate } from "react-router"; | import { Button } from "@/components/ui/button"; | ||||||
| import { useUserContext } from "../context/user-context"; | import { | ||||||
| import { Title, Paper, Text } from "@mantine/core"; |   Card, | ||||||
| import { Layout } from "../components/layouts/layout"; |   CardContent, | ||||||
| import { TotpForm } from "../components/auth/totp-form"; |   CardDescription, | ||||||
| import { useMutation } from "@tanstack/react-query"; |   CardHeader, | ||||||
| import axios from "axios"; |   CardTitle, | ||||||
| import { notifications } from "@mantine/notifications"; | } from "@/components/ui/card"; | ||||||
| import { useAppContext } from "../context/app-context"; | import { | ||||||
|  |   InputOTP, | ||||||
|  |   InputOTPGroup, | ||||||
|  |   InputOTPSeparator, | ||||||
|  |   InputOTPSlot, | ||||||
|  | } from "@/components/ui/input-otp"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
|  |  | ||||||
| export const TotpPage = () => { | export const TotpPage = () => { | ||||||
|   const queryString = window.location.search; |  | ||||||
|   const params = new URLSearchParams(queryString); |  | ||||||
|   const redirectUri = params.get("redirect_uri") ?? ""; |  | ||||||
|  |  | ||||||
|   const { totpPending, isLoggedIn } = useUserContext(); |  | ||||||
|   const { title } = useAppContext(); |  | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |  | ||||||
|   if (isLoggedIn) { |  | ||||||
|     return <Navigate to={`/logout`} />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (!totpPending) { |  | ||||||
|     return <Navigate to={`/login?redirect_uri=${redirectUri}`} />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const totpMutation = useMutation({ |  | ||||||
|     mutationFn: async (totp: { code: string }) => { |  | ||||||
|       await axios.post("/api/totp", totp); |  | ||||||
|     }, |  | ||||||
|     onError: () => { |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("totpFailTitle"), |  | ||||||
|         message: t("totpFailSubtitle"), |  | ||||||
|         color: "red", |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     onSuccess: () => { |  | ||||||
|       notifications.show({ |  | ||||||
|         title: t("totpSuccessTitle"), |  | ||||||
|         message: t("totpSuccessSubtitle"), |  | ||||||
|         color: "green", |  | ||||||
|       }); |  | ||||||
|       setTimeout(() => { |  | ||||||
|         window.location.replace(`/continue?redirect_uri=${redirectUri}`); |  | ||||||
|       }, 500); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Layout> |     <Card className="min-w-xs md:max-w-sm"> | ||||||
|       <Title ta="center">{title}</Title> |       <CardHeader> | ||||||
|       <Paper shadow="md" p="xl" mt={30} radius="md" withBorder> |         <CardTitle className="text-3xl">{t("totpTitle")}</CardTitle> | ||||||
|         <Text size="lg" fw={500} mb="md" ta="center"> |         <CardDescription>{t("totpSubtitle")}</CardDescription> | ||||||
|           {t("totpTitle")} |       </CardHeader> | ||||||
|         </Text> |       <CardContent className="flex flex-col gap-6 items-stretch"> | ||||||
|         <TotpForm |         <InputOTP maxLength={6}> | ||||||
|           isPending={totpMutation.isPending} |           <InputOTPGroup> | ||||||
|           onSubmit={(values) => totpMutation.mutate(values)} |             <InputOTPSlot index={0} /> | ||||||
|         /> |             <InputOTPSlot index={1} /> | ||||||
|       </Paper> |             <InputOTPSlot index={2} /> | ||||||
|     </Layout> |           </InputOTPGroup> | ||||||
|  |           <InputOTPSeparator /> | ||||||
|  |           <InputOTPGroup> | ||||||
|  |             <InputOTPSlot index={3} /> | ||||||
|  |             <InputOTPSlot index={4} /> | ||||||
|  |             <InputOTPSlot index={5} /> | ||||||
|  |           </InputOTPGroup> | ||||||
|  |         </InputOTP> | ||||||
|  |         <Button>{t("continueTitle")}</Button> | ||||||
|  |       </CardContent> | ||||||
|  |     </Card> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,56 +0,0 @@ | |||||||
| import { Button, Code, Paper, Text } from "@mantine/core"; |  | ||||||
| import { Layout } from "../components/layouts/layout"; |  | ||||||
| import { Navigate } from "react-router"; |  | ||||||
| import { isQueryValid } from "../utils/utils"; |  | ||||||
| import { Trans, useTranslation } from "react-i18next"; |  | ||||||
|  |  | ||||||
| export const UnauthorizedPage = () => { |  | ||||||
|   const queryString = window.location.search; |  | ||||||
|   const params = new URLSearchParams(queryString); |  | ||||||
|   const username = params.get("username") ?? ""; |  | ||||||
|   const resource = params.get("resource") ?? ""; |  | ||||||
|  |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|  |  | ||||||
|   if (!isQueryValid(username)) { |  | ||||||
|     return <Navigate to="/" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Layout> |  | ||||||
|       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> |  | ||||||
|         <Text size="xl" fw={700}> |  | ||||||
|           {t("Unauthorized")} |  | ||||||
|         </Text> |  | ||||||
|         <Text> |  | ||||||
|           {isQueryValid(resource) ? ( |  | ||||||
|             <Text> |  | ||||||
|               <Trans |  | ||||||
|                 i18nKey="unauthorizedResourceSubtitle" |  | ||||||
|                 t={t} |  | ||||||
|                 components={{ Code: <Code /> }} |  | ||||||
|                 values={{ resource, username }} |  | ||||||
|               /> |  | ||||||
|             </Text> |  | ||||||
|           ) : ( |  | ||||||
|             <Text> |  | ||||||
|               <Trans |  | ||||||
|                 i18nKey="unaothorizedLoginSubtitle" |  | ||||||
|                 t={t} |  | ||||||
|                 components={{ Code: <Code /> }} |  | ||||||
|                 values={{ username }} |  | ||||||
|               /> |  | ||||||
|             </Text> |  | ||||||
|           )} |  | ||||||
|         </Text> |  | ||||||
|         <Button |  | ||||||
|           fullWidth |  | ||||||
|           mt="xl" |  | ||||||
|           onClick={() => window.location.replace("/login")} |  | ||||||
|         > |  | ||||||
|           {t("unauthorizedButton")} |  | ||||||
|         </Button> |  | ||||||
|       </Paper> |  | ||||||
|     </Layout> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| import { z } from "zod"; |  | ||||||
|  |  | ||||||
| export const appContextSchema = z.object({ |  | ||||||
|   configuredProviders: z.array(z.string()), |  | ||||||
|   disableContinue: z.boolean(), |  | ||||||
|   title: z.string(), |  | ||||||
|   genericName: z.string(), |  | ||||||
|   domain: z.string(), |  | ||||||
|   forgotPasswordMessage: z.string(), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export type AppContextSchemaType = z.infer<typeof appContextSchema>; |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| import { z } from "zod"; |  | ||||||
|  |  | ||||||
| export const loginSchema = z.object({ |  | ||||||
|   username: z.string(), |  | ||||||
|   password: z.string(), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export type LoginFormValues = z.infer<typeof loginSchema>; |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| import { z } from "zod"; |  | ||||||
|  |  | ||||||
| export const userContextSchema = z.object({ |  | ||||||
|   isLoggedIn: z.boolean(), |  | ||||||
|   username: z.string(), |  | ||||||
|   oauth: z.boolean(), |  | ||||||
|   provider: z.string(), |  | ||||||
|   totpPending: z.boolean(), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export type UserContextSchemaType = z.infer<typeof userContextSchema>; |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); |  | ||||||
| export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null"; |  | ||||||
| export const escapeRegex = (value: string) => value.replace(/[-\/\\^$.*+?()[\]{}|]/g, "\\$&"); |  | ||||||
| @@ -1,5 +1,11 @@ | |||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|  |     // Resolve paths | ||||||
|  |     "baseUrl": ".", | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": ["./src/*"] | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||||
|     "target": "ES2020", |     "target": "ES2020", | ||||||
|     "useDefineForClassFields": true, |     "useDefineForClassFields": true, | ||||||
|   | |||||||
| @@ -3,5 +3,11 @@ | |||||||
|   "references": [ |   "references": [ | ||||||
|     { "path": "./tsconfig.app.json" }, |     { "path": "./tsconfig.app.json" }, | ||||||
|     { "path": "./tsconfig.node.json" } |     { "path": "./tsconfig.node.json" } | ||||||
|   ] |   ], | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "baseUrl": ".", | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": ["./src/*"] | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,17 +1,14 @@ | |||||||
| import { defineConfig } from "vite"; | import { defineConfig } from 'vite' | ||||||
| import react from "@vitejs/plugin-react-swc"; | import react from '@vitejs/plugin-react' | ||||||
|  | import path from "path" | ||||||
|  | import tailwindcss from "@tailwindcss/vite" | ||||||
|  |  | ||||||
| // https://vite.dev/config/ | // https://vite.dev/config/ | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   plugins: [react()], |   plugins: [react(), tailwindcss()], | ||||||
|   server: { |   resolve: { | ||||||
|     host: "0.0.0.0", |     alias: { | ||||||
|     proxy: { |       "@": path.resolve(__dirname, "./src"), | ||||||
|       "/api": { |     }, | ||||||
|         target: "http://tinyauth-backend:3000/api", |   }, | ||||||
|         changeOrigin: true, | }) | ||||||
|         rewrite: (path) => path.replace(/^\/api/, ""), |  | ||||||
|       }, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
|   | |||||||
 Stavros
					Stavros