diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-06-29 01:28:39 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-06-29 01:28:39 -0700 |
| commit | ff37cca46430ed714015647469f88ce06781457a (patch) | |
| tree | 63f406a3c7fb463fed7f34efc8d04d58fe96e0cb /frontend | |
| parent | e4fa1e69e7ebfb627c7198fd1a9881e9327ec4d4 (diff) | |
scaffold register,login,and auth endpoints
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/.gitignore | 1 | ||||
| -rw-r--r-- | frontend/package-lock.json | 747 | ||||
| -rw-r--r-- | frontend/package.json | 5 | ||||
| -rw-r--r-- | frontend/src/App.css | 42 | ||||
| -rw-r--r-- | frontend/src/App.tsx | 46 | ||||
| -rw-r--r-- | frontend/src/assets/react.svg | 1 | ||||
| -rw-r--r-- | frontend/src/contexts/AuthContext.tsx | 139 | ||||
| -rw-r--r-- | frontend/src/index.css | 69 | ||||
| -rw-r--r-- | frontend/src/main.tsx | 7 | ||||
| -rw-r--r-- | frontend/src/pages/Home.tsx | 119 | ||||
| -rw-r--r-- | frontend/src/pages/Landing.tsx | 154 | ||||
| -rw-r--r-- | frontend/src/pages/Login.tsx | 207 | ||||
| -rw-r--r-- | frontend/src/pages/Register.tsx | 230 | ||||
| -rw-r--r-- | frontend/src/utils/api.ts | 161 | ||||
| -rw-r--r-- | frontend/vite.config.ts | 3 |
15 files changed, 1717 insertions, 214 deletions
diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..7ceb59f 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d8a5110..aaaaa31 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,11 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.1.11", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-router": "^7.6.3", + "tailwindcss": "^4.1.11" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -29,7 +32,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -308,7 +310,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -324,7 +325,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -340,7 +340,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -356,7 +355,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -372,7 +370,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -388,7 +385,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -404,7 +400,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -420,7 +415,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -436,7 +430,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -452,7 +445,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -468,7 +460,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -484,7 +475,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -500,7 +490,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -516,7 +505,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -532,7 +520,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -548,7 +535,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -564,7 +550,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -580,7 +565,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -596,7 +580,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -612,7 +595,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -628,7 +610,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -644,7 +625,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -660,7 +640,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -676,7 +655,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -692,7 +670,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -917,11 +894,22 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -935,7 +923,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -944,7 +931,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -952,14 +938,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1013,7 +997,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -1026,7 +1009,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -1039,7 +1021,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1052,7 +1033,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1065,7 +1045,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -1078,7 +1057,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -1091,7 +1069,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1104,7 +1081,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1117,7 +1093,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1130,7 +1105,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1143,7 +1117,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1156,7 +1129,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1169,7 +1141,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1182,7 +1153,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1195,7 +1165,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1208,7 +1177,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1221,7 +1189,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1234,7 +1201,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1247,7 +1213,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1260,12 +1225,273 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1776,6 +2002,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1806,6 +2041,15 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1849,17 +2093,38 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.168", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.168.tgz", "integrity": "sha512-RUNQmFLNIWVW6+z32EJQ5+qx8ci6RGvdtDC0Ls+F89wz6I2AthpXF0w0DIrn2jpLX0/PU9ZCo+Qp7bg/EckJmA==", "dev": true }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -2202,7 +2467,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2245,6 +2509,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2330,6 +2600,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2412,6 +2691,234 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2442,6 +2949,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2476,6 +2992,42 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2486,7 +3038,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2592,8 +3143,7 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -2611,7 +3161,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2701,6 +3250,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz", + "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2724,7 +3295,6 @@ "version": "4.43.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", - "dev": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -2762,8 +3332,7 @@ "node_modules/rollup/node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" }, "node_modules/run-parallel": { "version": "1.2.0", @@ -2802,6 +3371,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2827,7 +3402,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2856,11 +3430,51 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" @@ -2876,7 +3490,6 @@ "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -2890,7 +3503,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, "engines": { "node": ">=12" }, @@ -3012,7 +3624,6 @@ "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "dev": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -3086,7 +3697,6 @@ "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -3100,7 +3710,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, "engines": { "node": ">=12" }, diff --git a/frontend/package.json b/frontend/package.json index 5c32b8a..533a099 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.1.11", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-router": "^7.6.3", + "tailwindcss": "^4.1.11" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3d7ded3..d317805 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,35 +1,21 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { Routes, Route } from 'react-router'; +import { AuthProvider } from './contexts/AuthContext'; +import Landing from './pages/Landing'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import Home from './pages/Home'; function App() { - const [count, setCount] = useState(0) - return ( - <> - <div> - <a href="https://vite.dev" target="_blank"> - <img src={viteLogo} className="logo" alt="Vite logo" /> - </a> - <a href="https://react.dev" target="_blank"> - <img src={reactLogo} className="logo react" alt="React logo" /> - </a> - </div> - <h1>Vite + React</h1> - <div className="card"> - <button onClick={() => setCount((count) => count + 1)}> - count is {count} - </button> - <p> - Edit <code>src/App.tsx</code> and save to test HMR - </p> - </div> - <p className="read-the-docs"> - Click on the Vite and React logos to learn more - </p> - </> - ) + <AuthProvider> + <Routes> + <Route path="/" element={<Landing />} /> + <Route path="/login" element={<Login />} /> + <Route path="/register" element={<Register />} /> + <Route path="/home" element={<Home />} /> + </Routes> + </AuthProvider> + ); } -export default App +export default App;
\ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..7e2668d --- /dev/null +++ b/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,139 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import type { ReactNode } from 'react'; +import { authApi } from '../utils/api'; +import type { User as ApiUser, SessionResponse } from '../utils/api'; + +interface User { + id: string; + username: string; + email: string; +} + +interface AuthContextType { + user: User | null; + isLoading: boolean; + isAuthenticated: boolean; + login: (username: string, password: string) => Promise<{ success: boolean; error?: string }>; + register: (userData: { username: string; email: string; password: string }) => Promise<{ success: boolean; error?: string }>; + logout: () => Promise<void>; + checkAuth: () => Promise<void>; +} + +const AuthContext = createContext<AuthContextType | undefined>(undefined); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { + const [user, setUser] = useState<User | null>(null); + const [isLoading, setIsLoading] = useState(true); + + const isAuthenticated = user !== null; + + const transformApiUser = (apiUser: ApiUser): User => ({ + id: apiUser.id.toString(), + username: apiUser.username, + email: apiUser.email, + }); + + const checkAuth = async () => { + try { + const response = await authApi.getSession(); + + if (response.error || !response.data) { + setUser(null); + return; + } + + const sessionData = response.data as SessionResponse; + + if (sessionData.authenticated && sessionData.user) { + setUser(transformApiUser(sessionData.user)); + } else { + setUser(null); + } + } catch (error) { + console.error('Auth check failed:', error); + setUser(null); + } finally { + setIsLoading(false); + } + }; + + const login = async (username: string, password: string) => { + try { + const response = await authApi.login({ username, password }); + + if (response.error) { + return { success: false, error: response.error }; + } + + if (response.data) { + setUser(transformApiUser(response.data as ApiUser)); + } + + return { success: true }; + } catch (error) { + console.error('Login failed:', error); + return { success: false, error: 'Network error. Please try again.' }; + } + }; + + const register = async (userData: { username: string; email: string; password: string }) => { + try { + const response = await authApi.register(userData); + + if (response.error) { + return { success: false, error: response.error }; + } + + if (response.data) { + setUser(transformApiUser(response.data as ApiUser)); + } + + return { success: true }; + } catch (error) { + console.error('Registration failed:', error); + return { success: false, error: 'Network error. Please try again.' }; + } + }; + + const logout = async () => { + try { + await authApi.logout(); + } catch (error) { + console.error('Logout error:', error); + } finally { + setUser(null); + } + }; + + useEffect(() => { + checkAuth(); + }, []); + + const value: AuthContextType = { + user, + isLoading, + isAuthenticated, + login, + register, + logout, + checkAuth, + }; + + return ( + <AuthContext.Provider value={value}> + {children} + </AuthContext.Provider> + ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index 08a3ac9..f1d8c73 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,68 +1 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} +@import "tailwindcss"; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..0467d26 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,13 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import App from './App.tsx' +import App from './App' +import { BrowserRouter } from "react-router"; createRoot(document.getElementById('root')!).render( <StrictMode> - <App /> + <BrowserRouter> + <App /> + </BrowserRouter> </StrictMode>, ) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..1d87c8a --- /dev/null +++ b/frontend/src/pages/Home.tsx @@ -0,0 +1,119 @@ +import { Link, useNavigate } from 'react-router'; +import { useAuth } from '../contexts/AuthContext'; + +const Home = () => { + const { user, isLoading, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = async () => { + try { + await logout(); + navigate('/'); + } catch (error) { + console.error('Logout failed:', error); + alert('Network error during logout. Please try again.'); + } + }; + + if (isLoading) { + return ( + <div className="min-h-screen bg-slate-950 flex items-center justify-center"> + <div className="text-center"> + <div className="w-8 h-8 border-2 border-violet-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div> + <p className="text-slate-300">Loading dashboard...</p> + </div> + </div> + ); + } + + if (!user) { + return ( + <div className="min-h-screen bg-slate-950 flex items-center justify-center"> + <div className="text-center max-w-md mx-auto px-6"> + <div className="bg-slate-900 rounded-lg p-8 border border-slate-700"> + <h2 className="text-2xl font-bold text-white mb-4">Session Expired</h2> + <p className="text-slate-300 mb-6">Please sign in to access your dashboard.</p> + <div className="space-y-3"> + <Link + to="/login" + className="block w-full bg-violet-600 hover:bg-violet-700 text-white py-3 rounded-md font-medium transition-colors" + > + Sign In + </Link> + <Link + to="/" + className="block w-full border border-slate-600 hover:border-violet-500 text-slate-300 hover:text-white py-3 rounded-md font-medium transition-colors" + > + Back to Home + </Link> + </div> + </div> + </div> + </div> + ); + } + + return ( + <div className="min-h-screen bg-slate-950"> + {/* Navigation */} + <nav className="border-b border-slate-800 bg-slate-950/95 backdrop-blur-sm sticky top-0 z-50"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="flex items-center justify-between h-16"> + <div className="flex items-center space-x-3"> + <div className="w-8 h-8 bg-violet-600 rounded-md flex items-center justify-center"> + <span className="text-white font-bold text-sm">M</span> + </div> + <span className="text-white font-semibold text-lg">Mirage</span> + </div> + <div className="flex items-center space-x-4"> + <span className="text-slate-300 text-sm">Welcome back, {user.username}</span> + <button + onClick={handleLogout} + className="text-slate-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors" + > + Sign Out + </button> + </div> + </div> + </div> + </nav> + + {/* Main Content */} + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> + {/* Header */} + <div className="mb-8"> + <h1 className="text-3xl font-bold text-white mb-2">Dashboard</h1> + <p className="text-slate-400">Track your rhythm game progress and performance</p> + </div> + + {/* Coming Soon Card */} + <div className="bg-slate-900 rounded-lg border border-slate-700 p-12 text-center"> + <div className="w-16 h-16 bg-violet-600/20 rounded-full flex items-center justify-center mx-auto mb-6"> + <svg className="w-8 h-8 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> + </svg> + </div> + <h2 className="text-2xl font-bold text-white mb-4">Dashboard Coming Soon</h2> + <p className="text-slate-300 mb-8 max-w-2xl mx-auto"> + We're working hard to bring you an amazing dashboard experience. Track your scores, + analyze your performance, and compete with friends - all coming soon! + </p> + <div className="flex flex-col sm:flex-row items-center justify-center gap-4"> + <div className="bg-slate-800 px-6 py-3 rounded-lg border border-slate-600"> + <p className="text-sm text-slate-300"> + <span className="font-semibold text-violet-300">User ID:</span> {user.id} + </p> + </div> + <div className="bg-slate-800 px-6 py-3 rounded-lg border border-slate-600"> + <p className="text-sm text-slate-300"> + <span className="font-semibold text-violet-300">Email:</span> {user.email} + </p> + </div> + </div> + </div> + </div> + </div> + ); +}; + +export default Home; diff --git a/frontend/src/pages/Landing.tsx b/frontend/src/pages/Landing.tsx new file mode 100644 index 0000000..7cd0f43 --- /dev/null +++ b/frontend/src/pages/Landing.tsx @@ -0,0 +1,154 @@ +import { Link } from 'react-router'; + +const Landing = () => { + return ( + <div className="min-h-screen bg-slate-950 text-slate-100"> + {/* Navigation */} + <nav className="border-b border-slate-800 bg-slate-950/95 backdrop-blur-sm sticky top-0 z-50"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="flex items-center justify-between h-16"> + <div className="flex items-center space-x-3"> + <div className="w-8 h-8 bg-violet-600 rounded-md flex items-center justify-center"> + <span className="text-white font-bold text-sm">M</span> + </div> + <span className="text-white font-semibold text-lg">Mirage</span> + </div> + <div className="flex items-center space-x-4"> + <Link + to="/login" + className="text-slate-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors" + > + Log In + </Link> + <Link + to="/register" + className="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors" + > + Create Account + </Link> + </div> + </div> + </div> + </nav> + + {/* Hero Section with Banner */} + <section className="relative py-32 lg:py-40 overflow-hidden"> + {/* Background Image */} + <div className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-20" + style={{backgroundImage: "url('https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80')"}}></div> + + {/* Overlay */} + <div className="absolute inset-0 bg-slate-950/60"></div> + + {/* Content */} + <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="max-w-4xl"> + <h1 className="text-5xl md:text-6xl font-bold text-white mb-6 leading-tight"> + Welcome to <span className="text-violet-400">Mirage!</span> + </h1> + <p className="text-xl text-slate-300 mb-8 leading-relaxed"> + Looks like you're not logged in. If you've got an account, <Link to="/login" className="text-violet-400 hover:text-violet-300 underline">Login!</Link> + </p> + </div> + </div> + </section> + + {/* Introduction Section */} + <section className="py-16 border-t border-slate-800"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="max-w-4xl"> + <h2 className="text-3xl md:text-4xl font-bold text-white mb-6"> + I'm New Around Here, What is this? + </h2> + <p className="text-lg text-slate-300 leading-relaxed"> + <span className="font-semibold text-violet-300">Mirage</span> is a Rhythm Game Score Tracker. That means we... + </p> + </div> + </div> + </section> + + {/* Track Your Scores Section */} + <section className="py-16 border-t border-slate-800"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="max-w-4xl"> + <h2 className="text-3xl md:text-4xl font-bold text-white mb-6"> + <span className="text-violet-400">Track</span> Your Scores. + </h2> + <p className="text-lg text-slate-300 leading-relaxed"> + Mirage supports a bunch of your favourite games, and integrates with many existing services to + make sure no score is lost to the void. Furthermore, it's backed by an Open-Source API, so your + scores are always available! + </p> + </div> + </div> + </section> + + {/* Analyse Your Scores Section */} + <section className="py-16 border-t border-slate-800"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="max-w-4xl"> + <h2 className="text-3xl md:text-4xl font-bold text-white mb-6"> + <span className="text-purple-400">Analyse</span> Your Scores. + </h2> + <p className="text-lg text-slate-300 leading-relaxed"> + Mirage analyses your scores for you, breaking them down into all the statistics you'll ever need. + No more spreadsheets! + </p> + </div> + </div> + </section> + + {/* Provide Cool Features Section */} + <section className="py-16 border-t border-slate-800"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="max-w-4xl"> + <h2 className="text-3xl md:text-4xl font-bold text-white mb-6"> + Provide <span className="text-violet-400">Cool Features</span>. + </h2> + <p className="text-lg text-slate-300 leading-relaxed"> + Mirage implements the features rhythm gamers already talk about. Break your scores down + into sessions, showcase your best metrics on your profile, study your progress on folders - it's all + there, and done for you! + </p> + </div> + </div> + </section> + + {/* Call to Action Section */} + <section className="py-20 border-t border-slate-800 bg-slate-900"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="text-center max-w-2xl mx-auto"> + <h2 className="text-3xl md:text-4xl font-bold text-white mb-8"> + Ready to Start <span className="text-violet-400">Tracking?</span> + </h2> + <div className="flex flex-col sm:flex-row items-center justify-center gap-4"> + <Link + to="/register" + className="bg-violet-600 hover:bg-violet-700 text-white px-8 py-3 rounded-md text-lg font-medium transition-colors min-w-[200px] shadow-lg shadow-violet-600/25" + > + Create Account + </Link> + <Link + to="/login" + className="border border-slate-600 hover:border-violet-500 text-slate-300 hover:text-white px-8 py-3 rounded-md text-lg font-medium transition-colors min-w-[200px]" + > + Log In + </Link> + </div> + </div> + </div> + </section> + + {/* Footer */} + <footer className="border-t border-slate-800 py-12 bg-slate-950"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="text-center text-slate-400"> + <a className="hover:underline" href="">GitHub</a> + </div> + </div> + </footer> + </div> + ); +}; + +export default Landing; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..5bbdefc --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,207 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router"; +import { useAuth } from "../contexts/AuthContext"; + +const Login = () => { + const [formData, setFormData] = useState({ + username: "", + password: "", + }); + const [errors, setErrors] = useState<Record<string, string>>({}); + const [isLoading, setIsLoading] = useState(false); + + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + // Clear error when user starts typing + if (errors[name]) { + setErrors((prev) => ({ + ...prev, + [name]: "", + })); + } + }; + + const validateForm = () => { + const newErrors: Record<string, string> = {}; + + if (!formData.username.trim()) { + newErrors.username = "Username is required"; + } else if (formData.username.length < 3) { + newErrors.username = "Username must be at least 3 characters"; + } + + if (!formData.password) { + newErrors.password = "Password is required"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const { login } = useAuth(); + const navigate = useNavigate(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsLoading(true); + try { + const result = await login(formData.username, formData.password); + + if (!result.success) { + setErrors({ + general: result.error || "Login failed. Please try again.", + }); + return; + } + + // Redirect to home page on successful login + navigate("/home"); + } catch (error) { + console.error("Login failed:", error); + setErrors({ general: "Network error. Please try again." }); + } finally { + setIsLoading(false); + } + }; + + return ( + <div className="min-h-screen bg-slate-950 flex items-center justify-center px-4 sm:px-6 lg:px-8"> + <div className="max-w-md w-full space-y-8"> + {/* Header */} + <div className="text-center"> + <Link to="/" className="inline-flex items-center space-x-3 mb-8"> + <div className="w-10 h-10 bg-violet-600 rounded-md flex items-center justify-center"> + <span className="text-white font-bold text-lg">M</span> + </div> + <span className="text-white font-semibold text-2xl">Mirage</span> + </Link> + <h2 className="text-3xl font-bold text-white"> + Sign in to your account + </h2> + <p className="mt-2 text-sm text-slate-400"> + Or{" "} + <Link + to="/register" + className="font-medium text-violet-400 hover:text-violet-300 transition-colors" + > + create a new account + </Link> + </p> + </div> + + {/* Form */} + <div className="bg-slate-900 rounded-lg p-8 border border-slate-700"> + {errors.general && ( + <div className="mb-6 bg-red-900/50 border border-red-700 rounded-md p-4"> + <div className="flex"> + <div className="flex-shrink-0"> + <svg + className="h-5 w-5 text-red-400" + viewBox="0 0 20 20" + fill="currentColor" + > + <path + fillRule="evenodd" + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" + clipRule="evenodd" + /> + </svg> + </div> + <div className="ml-3"> + <p className="text-sm text-red-300">{errors.general}</p> + </div> + </div> + </div> + )} + + <form onSubmit={handleSubmit} className="space-y-6"> + <div> + <label + htmlFor="username" + className="block text-sm font-medium text-slate-300 mb-2" + > + Username + </label> + <input + type="text" + id="username" + name="username" + value={formData.username} + onChange={handleChange} + className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors" + placeholder="Enter your username" + /> + {errors.username && ( + <p className="mt-1 text-sm text-red-400">{errors.username}</p> + )} + </div> + + <div> + <label + htmlFor="password" + className="block text-sm font-medium text-slate-300 mb-2" + > + Password + </label> + <input + type="password" + id="password" + name="password" + value={formData.password} + onChange={handleChange} + className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors" + placeholder="Enter your password" + /> + {errors.password && ( + <p className="mt-1 text-sm text-red-400">{errors.password}</p> + )} + </div> + + <button + type="submit" + disabled={isLoading} + className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-violet-600 hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-violet-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" + > + {isLoading ? ( + <div className="flex items-center"> + <svg + className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + > + <circle + className="opacity-25" + cx="12" + cy="12" + r="10" + stroke="currentColor" + strokeWidth="4" + ></circle> + <path + className="opacity-75" + fill="currentColor" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" + ></path> + </svg> + Signing in... + </div> + ) : ( + "Sign in" + )} + </button> + </form> + </div> + </div> + </div> + ); +}; + +export default Login; diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx new file mode 100644 index 0000000..1f8dde8 --- /dev/null +++ b/frontend/src/pages/Register.tsx @@ -0,0 +1,230 @@ +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router'; +import { useAuth } from '../contexts/AuthContext'; + +const Register = () => { + const [formData, setFormData] = useState({ + username: '', + email: '', + password: '', + confirmPassword: '' + }); + const [errors, setErrors] = useState<Record<string, string>>({}); + const [isLoading, setIsLoading] = useState(false); + + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + if (errors[name]) { + setErrors(prev => ({ + ...prev, + [name]: '' + })); + } + }; + + const validateForm = () => { + const newErrors: Record<string, string> = {}; + + if (!formData.username.trim()) { + newErrors.username = 'Username is required'; + } else if (formData.username.length < 3) { + newErrors.username = 'Username must be at least 3 characters'; + } + + if (!formData.email.trim()) { + newErrors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = 'Please enter a valid email address'; + } + + if (!formData.password) { + newErrors.password = 'Password is required'; + } else if (formData.password.length < 6) { + newErrors.password = 'Password must be at least 6 characters'; + } + + if (!formData.confirmPassword) { + newErrors.confirmPassword = 'Please confirm your password'; + } else if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = 'Passwords do not match'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const { register } = useAuth(); + const navigate = useNavigate(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsLoading(true); + try { + const result = await register({ + username: formData.username, + email: formData.email, + password: formData.password, + }); + + if (!result.success) { + setErrors({ general: result.error || 'Registration failed. Please try again.' }); + return; + } + + // Redirect to home page on successful registration + navigate('/home'); + } catch (error) { + console.error('Registration failed:', error); + setErrors({ general: 'Network error. Please try again.' }); + } finally { + setIsLoading(false); + } + }; + + return ( + <div className="min-h-screen bg-slate-950 flex items-center justify-center px-4 sm:px-6 lg:px-8"> + <div className="max-w-md w-full space-y-8"> + {/* Header */} + <div className="text-center"> + <Link to="/" className="inline-flex items-center space-x-3 mb-8"> + <div className="w-10 h-10 bg-violet-600 rounded-md flex items-center justify-center"> + <span className="text-white font-bold text-lg">M</span> + </div> + <span className="text-white font-semibold text-2xl">Mirage</span> + </Link> + <h2 className="text-3xl font-bold text-white"> + Create your account + </h2> + <p className="mt-2 text-sm text-slate-400"> + Already have an account?{' '} + <Link + to="/login" + className="font-medium text-violet-400 hover:text-violet-300 transition-colors" + > + Sign in here + </Link> + </p> + </div> + + {/* Form */} + <div className="bg-slate-900 rounded-lg p-8 border border-slate-700"> + {errors.general && ( + <div className="mb-6 bg-red-900/50 border border-red-700 rounded-md p-4"> + <div className="flex"> + <div className="flex-shrink-0"> + <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> + <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /> + </svg> + </div> + <div className="ml-3"> + <p className="text-sm text-red-300">{errors.general}</p> + </div> + </div> + </div> + )} + + <form onSubmit={handleSubmit} className="space-y-6"> + <div> + <label htmlFor="username" className="block text-sm font-medium text-slate-300 mb-2"> + Username + </label> + <input + type="text" + id="username" + name="username" + value={formData.username} + onChange={handleChange} + className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors" + placeholder="Choose a username" + /> + {errors.username && ( + <p className="mt-1 text-sm text-red-400">{errors.username}</p> + )} + </div> + + <div> + <label htmlFor="email" className="block text-sm font-medium text-slate-300 mb-2"> + Email address + </label> + <input + type="email" + id="email" + name="email" + value={formData.email} + onChange={handleChange} + className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors" + placeholder="Enter your email" + /> + {errors.email && ( + <p className="mt-1 text-sm text-red-400">{errors.email}</p> + )} + </div> + + <div> + <label htmlFor="password" className="block text-sm font-medium text-slate-300 mb-2"> + Password + </label> + <input + type="password" + id="password" + name="password" + value={formData.password} + onChange={handleChange} + className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors" + placeholder="Create a password" + /> + {errors.password && ( + <p className="mt-1 text-sm text-red-400">{errors.password}</p> + )} + </div> + + <div> + <label htmlFor="confirmPassword" className="block text-sm font-medium text-slate-300 mb-2"> + Confirm password + </label> + <input + type="password" + id="confirmPassword" + name="confirmPassword" + value={formData.confirmPassword} + onChange={handleChange} + className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors" + placeholder="Confirm your password" + /> + {errors.confirmPassword && ( + <p className="mt-1 text-sm text-red-400">{errors.confirmPassword}</p> + )} + </div> + + <button + type="submit" + disabled={isLoading} + className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-violet-600 hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-violet-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" + > + {isLoading ? ( + <div className="flex items-center"> + <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> + <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> + <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> + </svg> + Creating account... + </div> + ) : ( + 'Create account' + )} + </button> + </form> + </div> + </div> + </div> + ); +}; + +export default Register; diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts new file mode 100644 index 0000000..528c170 --- /dev/null +++ b/frontend/src/utils/api.ts @@ -0,0 +1,161 @@ +const API_BASE_URL = import.meta.env.VITE_API_URL; + +// Auth API functions +export const authApi = { + async login(credentials: { username: string; password: string }) { + try { + const response = await fetch(`${API_BASE_URL}/authenticate`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(credentials), + }); + + const data = await response.json(); + + if (!response.ok) { + return { error: data.error || `HTTP ${response.status}` }; + } + + return { data }; + } catch (error) { + console.error('Login failed:', error); + return { error: 'Network error. Please check your connection.' }; + } + }, + + async register(userData: { + username: string; + email: string; + password: string; + }) { + try { + const response = await fetch(`${API_BASE_URL}/register`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData), + }); + + const data = await response.json(); + + if (!response.ok) { + return { error: data.error || `HTTP ${response.status}` }; + } + + return { data }; + } catch (error) { + console.error('Registration failed:', error); + return { error: 'Network error. Please check your connection.' }; + } + }, + + async logout() { + try { + const response = await fetch(`${API_BASE_URL}/logout`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (!response.ok) { + return { error: data.error || `HTTP ${response.status}` }; + } + + return { data }; + } catch (error) { + console.error('Logout failed:', error); + return { error: 'Network error. Please check your connection.' }; + } + }, + + async getSession() { + try { + const response = await fetch(`${API_BASE_URL}/session`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (!response.ok) { + return { error: data.error || `HTTP ${response.status}` }; + } + + return { data }; + } catch (error) { + console.error('Session check failed:', error); + return { error: 'Network error. Please check your connection.' }; + } + }, + + async getCurrentUser() { + try { + const response = await fetch(`${API_BASE_URL}/me`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (!response.ok) { + return { error: data.error || `HTTP ${response.status}` }; + } + + return { data }; + } catch (error) { + console.error('Get current user failed:', error); + return { error: 'Network error. Please check your connection.' }; + } + }, +}; + +export const infoApi = { + async getUsers() { + try { + const response = await fetch(`${API_BASE_URL}/users`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (!response.ok) { + return { error: data.error || `HTTP ${response.status}` }; + } + + return { data }; + } catch (error) { + console.error('Get users failed:', error); + return { error: 'Network error. Please check your connection.' }; + } + }, +}; + +export interface User { + id: number; + username: string; + email: string; +} + +export interface SessionResponse { + authenticated: boolean; + user?: User; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b0f57b..c4069b7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], }) |
