implement foundation + room system (Plan 1 of 5)
Bun workspace monorepo with shared, server, client packages. Server: Hono + @hono/node-ws, Drizzle + PostgreSQL, in-memory room manager with WebSocket broadcasting, HTTP room creation, DB persistence layer. Client: React 19 + Vite + Tailwind v4 + shadcn/ui, TanStack Router with landing/display/host/player routes, Zustand store, WebSocket connection hook. 20 tests passing (room manager unit + WS integration). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DATABASE_URL=postgresql://localhost:5433/celebrate_esc
|
||||||
|
PORT=3001
|
||||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1 +1,23 @@
|
|||||||
|
# worktrees
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# mise
|
||||||
|
.mise.local.toml
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|||||||
3
.mise.toml
Normal file
3
.mise.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[tools]
|
||||||
|
bun = "1.3.10"
|
||||||
|
node = "22.14.0"
|
||||||
24
biome.json
Normal file
24
biome.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "tab",
|
||||||
|
"lineWidth": 120
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedImports": "error",
|
||||||
|
"noUnusedVariables": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignore": ["node_modules", "dist", "drizzle"]
|
||||||
|
}
|
||||||
|
}
|
||||||
749
bun.lock
Normal file
749
bun.lock
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "celebrate-esc",
|
||||||
|
},
|
||||||
|
"packages/client": {
|
||||||
|
"name": "@celebrate-esc/client",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@celebrate-esc/shared": "workspace:*",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@tanstack/react-router": "latest",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.577.0",
|
||||||
|
"react": "latest",
|
||||||
|
"react-dom": "latest",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"zustand": "latest",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "latest",
|
||||||
|
"@tanstack/router-plugin": "latest",
|
||||||
|
"@types/react": "latest",
|
||||||
|
"@types/react-dom": "latest",
|
||||||
|
"@vitejs/plugin-react": "latest",
|
||||||
|
"tailwindcss": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
"vite": "latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/server": {
|
||||||
|
"name": "@celebrate-esc/server",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@celebrate-esc/shared": "workspace:*",
|
||||||
|
"@hono/node-server": "latest",
|
||||||
|
"@hono/node-ws": "latest",
|
||||||
|
"@hono/zod-validator": "latest",
|
||||||
|
"drizzle-orm": "latest",
|
||||||
|
"hono": "latest",
|
||||||
|
"pg": "latest",
|
||||||
|
"zod": "latest",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "latest",
|
||||||
|
"drizzle-kit": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
"vitest": "latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/shared": {
|
||||||
|
"name": "@celebrate-esc/shared",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "latest",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "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-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
|
"@celebrate-esc/client": ["@celebrate-esc/client@workspace:packages/client"],
|
||||||
|
|
||||||
|
"@celebrate-esc/server": ["@celebrate-esc/server@workspace:packages/server"],
|
||||||
|
|
||||||
|
"@celebrate-esc/shared": ["@celebrate-esc/shared@workspace:packages/shared"],
|
||||||
|
|
||||||
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||||
|
|
||||||
|
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||||
|
|
||||||
|
"@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
|
||||||
|
|
||||||
|
"@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.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-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||||
|
|
||||||
|
"@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-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-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@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-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@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-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "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-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||||
|
|
||||||
|
"@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-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=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
|
||||||
|
|
||||||
|
"@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="],
|
||||||
|
|
||||||
|
"@tanstack/react-router": ["@tanstack/react-router@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.166.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-LLcXu2nrCn2WL+w0YAbg3CRZIIO2cYVSC3y+ZYlFBxBs4hh8eoNP1EWFvRLZGCFYpqON7x6qUf1u0W7tH0cJJw=="],
|
||||||
|
|
||||||
|
"@tanstack/react-store": ["@tanstack/react-store@0.9.2", "", { "dependencies": { "@tanstack/store": "0.9.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="],
|
||||||
|
|
||||||
|
"@tanstack/router-core": ["@tanstack/router-core@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-MCc8wYIIcxmbeidM8PL2QeaAjUIHyhEDIZPW6NGfn/uwvyi+K2ucn3AGCxxcXl4JGGm0Mx9+7buYl1v3HdcFrg=="],
|
||||||
|
|
||||||
|
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.7", "", { "dependencies": { "@tanstack/router-core": "1.166.7", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-lBI0VS7J1zMrJhfvT+3FMq9jPdOrJ3VgciPXyYvZBF/a9Mr8T94MU78PqrBNuJbYh7qCFO14ZhArUFqkYGuozQ=="],
|
||||||
|
|
||||||
|
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.166.7", "@tanstack/router-generator": "1.166.7", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.166.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-R06qe5UwApb/u02wDITVxN++6QE4xsLFQCr029VZ+4V8gyIe35kr8UCg3Jiyl6D5GXxhj62U2Ei8jccdkQaivw=="],
|
||||||
|
|
||||||
|
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.4", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-r8TpjyIZoqrXXaf2DDyjd44gjGBoyE+/oEaaH68yLI9ySPO1gUWmQENZ1MZnmBnpUGN24NOZxdjDLc8npK0SAw=="],
|
||||||
|
|
||||||
|
"@tanstack/store": ["@tanstack/store@0.9.2", "", {}, "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA=="],
|
||||||
|
|
||||||
|
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.4", "", {}, "sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w=="],
|
||||||
|
|
||||||
|
"@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.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="],
|
||||||
|
|
||||||
|
"@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
|
||||||
|
|
||||||
|
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||||
|
|
||||||
|
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
|
||||||
|
|
||||||
|
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
|
||||||
|
|
||||||
|
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
|
||||||
|
|
||||||
|
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
|
||||||
|
|
||||||
|
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||||
|
|
||||||
|
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
|
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||||
|
|
||||||
|
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||||
|
|
||||||
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
|
||||||
|
|
||||||
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||||
|
|
||||||
|
"drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="],
|
||||||
|
|
||||||
|
"drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
|
||||||
|
|
||||||
|
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||||
|
|
||||||
|
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
|
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"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-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="],
|
||||||
|
|
||||||
|
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"isbot": ["isbot@5.1.36", "", {}, "sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="],
|
||||||
|
|
||||||
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
|
"pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
|
||||||
|
|
||||||
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
|
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="],
|
||||||
|
|
||||||
|
"seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="],
|
||||||
|
|
||||||
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||||
|
|
||||||
|
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
|
|
||||||
|
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
|
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
|
||||||
|
|
||||||
|
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "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-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
|
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
||||||
|
|
||||||
|
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||||
|
|
||||||
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||||
|
|
||||||
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||||
|
|
||||||
|
"drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "celebrate-esc",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/shared",
|
||||||
|
"packages/server",
|
||||||
|
"packages/client"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --filter '*' dev",
|
||||||
|
"dev:server": "bun --filter server dev",
|
||||||
|
"dev:client": "bun --filter client dev",
|
||||||
|
"build": "bun --filter '*' build",
|
||||||
|
"test": "bun --filter '*' test",
|
||||||
|
"lint": "biome check .",
|
||||||
|
"format": "biome check --write ."
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/client/components.json
Normal file
20
packages/client/components.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
12
packages/client/index.html
Normal file
12
packages/client/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ESC Party</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
packages/client/package.json
Normal file
35
packages/client/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@celebrate-esc/client",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@celebrate-esc/shared": "workspace:*",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@tanstack/react-router": "latest",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.577.0",
|
||||||
|
"react": "latest",
|
||||||
|
"react-dom": "latest",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"zustand": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "latest",
|
||||||
|
"@tanstack/router-plugin": "latest",
|
||||||
|
"@types/react": "latest",
|
||||||
|
"@types/react-dom": "latest",
|
||||||
|
"@vitejs/plugin-react": "latest",
|
||||||
|
"tailwindcss": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
"vite": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/client/src/app.css
Normal file
40
packages/client/src/app.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-background: hsl(0 0% 100%);
|
||||||
|
--color-foreground: hsl(0 0% 3.9%);
|
||||||
|
--color-card: hsl(0 0% 100%);
|
||||||
|
--color-card-foreground: hsl(0 0% 3.9%);
|
||||||
|
--color-popover: hsl(0 0% 100%);
|
||||||
|
--color-popover-foreground: hsl(0 0% 3.9%);
|
||||||
|
--color-primary: hsl(0 0% 9%);
|
||||||
|
--color-primary-foreground: hsl(0 0% 98%);
|
||||||
|
--color-secondary: hsl(0 0% 96.1%);
|
||||||
|
--color-secondary-foreground: hsl(0 0% 9%);
|
||||||
|
--color-muted: hsl(0 0% 96.1%);
|
||||||
|
--color-muted-foreground: hsl(0 0% 45.1%);
|
||||||
|
--color-accent: hsl(0 0% 96.1%);
|
||||||
|
--color-accent-foreground: hsl(0 0% 9%);
|
||||||
|
--color-destructive: hsl(0 84.2% 60.2%);
|
||||||
|
--color-destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--color-border: hsl(0 0% 89.8%);
|
||||||
|
--color-input: hsl(0 0% 89.8%);
|
||||||
|
--color-ring: hsl(0 0% 3.9%);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/client/src/components/player-list.tsx
Normal file
35
packages/client/src/components/player-list.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import type { Player } from "@celebrate-esc/shared"
|
||||||
|
|
||||||
|
interface PlayerListProps {
|
||||||
|
players: Player[]
|
||||||
|
mySessionId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlayerList({ players, mySessionId }: PlayerListProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground">Players ({players.length})</h3>
|
||||||
|
<ul className="flex flex-col gap-1">
|
||||||
|
{players.map((player) => (
|
||||||
|
<li key={player.id} className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`h-2 w-2 rounded-full ${player.connected ? "bg-green-500" : "bg-muted"}`}
|
||||||
|
/>
|
||||||
|
<span className={player.sessionId === mySessionId ? "font-bold" : ""}>
|
||||||
|
{player.displayName}
|
||||||
|
</span>
|
||||||
|
{player.isHost && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Host
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{player.sessionId === mySessionId && (
|
||||||
|
<span className="text-xs text-muted-foreground">(you)</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
37
packages/client/src/components/room-header.tsx
Normal file
37
packages/client/src/components/room-header.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import type { Act } from "@celebrate-esc/shared"
|
||||||
|
|
||||||
|
interface RoomHeaderProps {
|
||||||
|
roomCode: string
|
||||||
|
currentAct: Act
|
||||||
|
connectionStatus: "disconnected" | "connecting" | "connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
const actLabels: Record<Act, string> = {
|
||||||
|
lobby: "Lobby",
|
||||||
|
act1: "Act 1",
|
||||||
|
act2: "Act 2",
|
||||||
|
act3: "Act 3",
|
||||||
|
ended: "Ended",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomHeader({ roomCode, currentAct, connectionStatus }: RoomHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between border-b p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="font-mono text-2xl font-bold tracking-widest">{roomCode}</span>
|
||||||
|
<Badge variant="outline">{actLabels[currentAct]}</Badge>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`h-2 w-2 rounded-full ${
|
||||||
|
connectionStatus === "connected"
|
||||||
|
? "bg-green-500"
|
||||||
|
: connectionStatus === "connecting"
|
||||||
|
? "bg-yellow-500"
|
||||||
|
: "bg-red-500"
|
||||||
|
}`}
|
||||||
|
title={connectionStatus}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
packages/client/src/components/ui/badge.tsx
Normal file
36
packages/client/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
56
packages/client/src/components/ui/button.tsx
Normal file
56
packages/client/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
79
packages/client/src/components/ui/card.tsx
Normal file
79
packages/client/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
22
packages/client/src/components/ui/input.tsx
Normal file
22
packages/client/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
53
packages/client/src/components/ui/tabs.tsx
Normal file
53
packages/client/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
103
packages/client/src/hooks/use-websocket.ts
Normal file
103
packages/client/src/hooks/use-websocket.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from "react"
|
||||||
|
import type { ClientMessage, ServerMessage } from "@celebrate-esc/shared"
|
||||||
|
import { useRoomStore } from "@/stores/room-store"
|
||||||
|
|
||||||
|
const SESSION_KEY = "esc-party-session"
|
||||||
|
|
||||||
|
function getStoredSession(): { roomCode: string; sessionId: string } | null {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(SESSION_KEY)
|
||||||
|
if (!raw) return null
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeSession(roomCode: string, sessionId: string) {
|
||||||
|
sessionStorage.setItem(SESSION_KEY, JSON.stringify({ roomCode, sessionId }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWebSocket(roomCode: string) {
|
||||||
|
const wsRef = useRef<WebSocket | null>(null)
|
||||||
|
const {
|
||||||
|
setRoom,
|
||||||
|
setMySessionId,
|
||||||
|
setConnectionStatus,
|
||||||
|
updatePlayerConnected,
|
||||||
|
addPlayer,
|
||||||
|
setAct,
|
||||||
|
reset,
|
||||||
|
} = useRoomStore()
|
||||||
|
|
||||||
|
const send = useCallback((message: ClientMessage) => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.send(JSON.stringify(message))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = getStoredSession()
|
||||||
|
const sessionId = stored?.roomCode === roomCode ? stored.sessionId : null
|
||||||
|
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||||
|
const wsUrl = sessionId
|
||||||
|
? `${protocol}//${window.location.host}/api/ws/${roomCode}?sessionId=${sessionId}`
|
||||||
|
: `${protocol}//${window.location.host}/api/ws/${roomCode}`
|
||||||
|
|
||||||
|
setConnectionStatus("connecting")
|
||||||
|
const ws = new WebSocket(wsUrl)
|
||||||
|
wsRef.current = ws
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
setConnectionStatus("connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const msg = JSON.parse(event.data) as ServerMessage
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case "room_state": {
|
||||||
|
setRoom(msg.room)
|
||||||
|
if (msg.sessionId) {
|
||||||
|
setMySessionId(msg.sessionId)
|
||||||
|
storeSession(roomCode, msg.sessionId)
|
||||||
|
} else if (sessionId) {
|
||||||
|
// Reconnected with stored session
|
||||||
|
setMySessionId(sessionId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "player_joined":
|
||||||
|
addPlayer(msg.player)
|
||||||
|
break
|
||||||
|
case "player_disconnected":
|
||||||
|
updatePlayerConnected(msg.playerId, false)
|
||||||
|
break
|
||||||
|
case "player_reconnected":
|
||||||
|
updatePlayerConnected(msg.playerId, true)
|
||||||
|
break
|
||||||
|
case "act_changed":
|
||||||
|
setAct(msg.newAct)
|
||||||
|
break
|
||||||
|
case "room_ended":
|
||||||
|
setAct("ended")
|
||||||
|
break
|
||||||
|
case "error":
|
||||||
|
console.error("Server error:", msg.message)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
setConnectionStatus("disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ws.close()
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
}, [roomCode, setRoom, setMySessionId, setConnectionStatus, updatePlayerConnected, addPlayer, setAct, reset])
|
||||||
|
|
||||||
|
return { send }
|
||||||
|
}
|
||||||
6
packages/client/src/lib/utils.ts
Normal file
6
packages/client/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
19
packages/client/src/main.tsx
Normal file
19
packages/client/src/main.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import "./app.css"
|
||||||
|
import { StrictMode } from "react"
|
||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
import { RouterProvider, createRouter } from "@tanstack/react-router"
|
||||||
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
|
||||||
|
const router = createRouter({ routeTree })
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
118
packages/client/src/routeTree.gen.ts
Normal file
118
packages/client/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as PlayRoomCodeRouteImport } from './routes/play.$roomCode'
|
||||||
|
import { Route as HostRoomCodeRouteImport } from './routes/host.$roomCode'
|
||||||
|
import { Route as DisplayRoomCodeRouteImport } from './routes/display.$roomCode'
|
||||||
|
|
||||||
|
const IndexRoute = IndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlayRoomCodeRoute = PlayRoomCodeRouteImport.update({
|
||||||
|
id: '/play/$roomCode',
|
||||||
|
path: '/play/$roomCode',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const HostRoomCodeRoute = HostRoomCodeRouteImport.update({
|
||||||
|
id: '/host/$roomCode',
|
||||||
|
path: '/host/$roomCode',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const DisplayRoomCodeRoute = DisplayRoomCodeRouteImport.update({
|
||||||
|
id: '/display/$roomCode',
|
||||||
|
path: '/display/$roomCode',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/display/$roomCode': typeof DisplayRoomCodeRoute
|
||||||
|
'/host/$roomCode': typeof HostRoomCodeRoute
|
||||||
|
'/play/$roomCode': typeof PlayRoomCodeRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/display/$roomCode': typeof DisplayRoomCodeRoute
|
||||||
|
'/host/$roomCode': typeof HostRoomCodeRoute
|
||||||
|
'/play/$roomCode': typeof PlayRoomCodeRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/display/$roomCode': typeof DisplayRoomCodeRoute
|
||||||
|
'/host/$roomCode': typeof HostRoomCodeRoute
|
||||||
|
'/play/$roomCode': typeof PlayRoomCodeRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths: '/' | '/display/$roomCode' | '/host/$roomCode' | '/play/$roomCode'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to: '/' | '/display/$roomCode' | '/host/$roomCode' | '/play/$roomCode'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/display/$roomCode'
|
||||||
|
| '/host/$roomCode'
|
||||||
|
| '/play/$roomCode'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
DisplayRoomCodeRoute: typeof DisplayRoomCodeRoute
|
||||||
|
HostRoomCodeRoute: typeof HostRoomCodeRoute
|
||||||
|
PlayRoomCodeRoute: typeof PlayRoomCodeRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/play/$roomCode': {
|
||||||
|
id: '/play/$roomCode'
|
||||||
|
path: '/play/$roomCode'
|
||||||
|
fullPath: '/play/$roomCode'
|
||||||
|
preLoaderRoute: typeof PlayRoomCodeRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/host/$roomCode': {
|
||||||
|
id: '/host/$roomCode'
|
||||||
|
path: '/host/$roomCode'
|
||||||
|
fullPath: '/host/$roomCode'
|
||||||
|
preLoaderRoute: typeof HostRoomCodeRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/display/$roomCode': {
|
||||||
|
id: '/display/$roomCode'
|
||||||
|
path: '/display/$roomCode'
|
||||||
|
fullPath: '/display/$roomCode'
|
||||||
|
preLoaderRoute: typeof DisplayRoomCodeRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
DisplayRoomCodeRoute: DisplayRoomCodeRoute,
|
||||||
|
HostRoomCodeRoute: HostRoomCodeRoute,
|
||||||
|
PlayRoomCodeRoute: PlayRoomCodeRoute,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
9
packages/client/src/routes/__root.tsx
Normal file
9
packages/client/src/routes/__root.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createRootRoute, Outlet } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: () => (
|
||||||
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
56
packages/client/src/routes/display.$roomCode.tsx
Normal file
56
packages/client/src/routes/display.$roomCode.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { useWebSocket } from "@/hooks/use-websocket"
|
||||||
|
import { useRoomStore } from "@/stores/room-store"
|
||||||
|
import { PlayerList } from "@/components/player-list"
|
||||||
|
import { RoomHeader } from "@/components/room-header"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/display/$roomCode")({
|
||||||
|
component: DisplayView,
|
||||||
|
})
|
||||||
|
|
||||||
|
function DisplayView() {
|
||||||
|
const { roomCode } = Route.useParams()
|
||||||
|
useWebSocket(roomCode)
|
||||||
|
const { room, connectionStatus } = useRoomStore()
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{connectionStatus === "connecting" ? "Connecting..." : "Room not found"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<RoomHeader roomCode={roomCode} currentAct={room.currentAct} connectionStatus={connectionStatus} />
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center gap-8 p-8">
|
||||||
|
{room.currentAct === "lobby" && <LobbyDisplay roomCode={roomCode} />}
|
||||||
|
<PlayerList players={room.players} mySessionId={null} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LobbyDisplay({ roomCode }: { roomCode: string }) {
|
||||||
|
const joinUrl = `${window.location.origin}/play/${roomCode}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
<h2 className="text-2xl text-muted-foreground">Join the party!</h2>
|
||||||
|
<div className="rounded-lg border-4 border-dashed border-muted p-8">
|
||||||
|
<span className="font-mono text-8xl font-bold tracking-[0.3em]">{roomCode}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Go to <span className="font-mono font-medium">{joinUrl}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">or scan the QR code</p>
|
||||||
|
{/* QR code will be added in Plan 5 (polish) */}
|
||||||
|
<div className="flex h-48 w-48 items-center justify-center rounded-lg border-2 border-dashed border-muted">
|
||||||
|
<span className="text-sm text-muted-foreground">QR code</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
87
packages/client/src/routes/host.$roomCode.tsx
Normal file
87
packages/client/src/routes/host.$roomCode.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { useWebSocket } from "@/hooks/use-websocket"
|
||||||
|
import { useRoomStore } from "@/stores/room-store"
|
||||||
|
import { PlayerList } from "@/components/player-list"
|
||||||
|
import { RoomHeader } from "@/components/room-header"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import type { Act } from "@celebrate-esc/shared"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/host/$roomCode")({
|
||||||
|
component: HostView,
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextActLabels: Partial<Record<Act, string>> = {
|
||||||
|
lobby: "Start Act 1",
|
||||||
|
act1: "Start Act 2",
|
||||||
|
act2: "Start Act 3",
|
||||||
|
act3: "End Party",
|
||||||
|
}
|
||||||
|
|
||||||
|
function HostView() {
|
||||||
|
const { roomCode } = Route.useParams()
|
||||||
|
const { send } = useWebSocket(roomCode)
|
||||||
|
const { room, mySessionId, connectionStatus } = useRoomStore()
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{connectionStatus === "connecting" ? "Connecting..." : "Room not found"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<RoomHeader roomCode={roomCode} currentAct={room.currentAct} connectionStatus={connectionStatus} />
|
||||||
|
<Tabs defaultValue="host" className="flex-1">
|
||||||
|
<TabsList className="w-full rounded-none">
|
||||||
|
<TabsTrigger value="play" className="flex-1">
|
||||||
|
Play
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="host" className="flex-1">
|
||||||
|
Host
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="play" className="p-4">
|
||||||
|
<PlayerList players={room.players} mySessionId={mySessionId} />
|
||||||
|
{/* Game UI will be added in later plans */}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="host" className="p-4">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Room Controls</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
{room.currentAct !== "ended" && (
|
||||||
|
<Button onClick={() => send({ type: "advance_act" })} className="w-full">
|
||||||
|
{nextActLabels[room.currentAct] ?? "Next"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{room.currentAct !== "ended" && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => send({ type: "end_room" })}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
End Party
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{room.currentAct === "ended" && (
|
||||||
|
<p className="text-center text-muted-foreground">
|
||||||
|
The party has ended. Thanks for playing!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<PlayerList players={room.players} mySessionId={mySessionId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
123
packages/client/src/routes/index.tsx
Normal file
123
packages/client/src/routes/index.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { useRoomStore } from "@/stores/room-store"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: LandingPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
function LandingPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center gap-8 p-4">
|
||||||
|
<h1 className="text-5xl font-bold tracking-tight">ESC Party</h1>
|
||||||
|
<p className="text-muted-foreground text-lg">Eurovision Song Contest — Party Companion</p>
|
||||||
|
<div className="flex flex-col gap-6 sm:flex-row">
|
||||||
|
<CreateRoomCard />
|
||||||
|
<JoinRoomCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateRoomCard() {
|
||||||
|
const [displayName, setDisplayName] = useState("")
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const setMySessionId = useRoomStore((s) => s.setMySessionId)
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
if (!displayName.trim()) return
|
||||||
|
setLoading(true)
|
||||||
|
setError("")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/rooms", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ displayName: displayName.trim() }),
|
||||||
|
})
|
||||||
|
const { data } = (await res.json()) as { data: { code: string; sessionId: string } }
|
||||||
|
|
||||||
|
// Store session for reconnection
|
||||||
|
sessionStorage.setItem(
|
||||||
|
"esc-party-session",
|
||||||
|
JSON.stringify({
|
||||||
|
roomCode: data.code,
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
setMySessionId(data.sessionId)
|
||||||
|
|
||||||
|
navigate({ to: "/host/$roomCode", params: { roomCode: data.code } })
|
||||||
|
} catch {
|
||||||
|
setError("Failed to create room")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-80">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Host a Party</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
<Input
|
||||||
|
placeholder="Your name"
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
maxLength={20}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleCreate} disabled={!displayName.trim() || loading}>
|
||||||
|
{loading ? "Creating..." : "Create Room"}
|
||||||
|
</Button>
|
||||||
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function JoinRoomCard() {
|
||||||
|
const [roomCode, setRoomCode] = useState("")
|
||||||
|
const [displayName, setDisplayName] = useState("")
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
function handleJoin() {
|
||||||
|
if (!roomCode.trim() || !displayName.trim()) return
|
||||||
|
// Store display name temporarily — will be sent via WebSocket on connect
|
||||||
|
sessionStorage.setItem("esc-party-join-name", displayName.trim())
|
||||||
|
navigate({ to: "/play/$roomCode", params: { roomCode: roomCode.trim().toUpperCase() } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-80">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Join a Party</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
<Input
|
||||||
|
placeholder="Room code"
|
||||||
|
value={roomCode}
|
||||||
|
onChange={(e) => setRoomCode(e.target.value.toUpperCase())}
|
||||||
|
maxLength={4}
|
||||||
|
className="text-center text-2xl tracking-widest"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Your name"
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
maxLength={20}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleJoin()}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleJoin} disabled={roomCode.length !== 4 || !displayName.trim()}>
|
||||||
|
Join Room
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
103
packages/client/src/routes/play.$roomCode.tsx
Normal file
103
packages/client/src/routes/play.$roomCode.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { useWebSocket } from "@/hooks/use-websocket"
|
||||||
|
import { useRoomStore } from "@/stores/room-store"
|
||||||
|
import { PlayerList } from "@/components/player-list"
|
||||||
|
import { RoomHeader } from "@/components/room-header"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/play/$roomCode")({
|
||||||
|
component: PlayerView,
|
||||||
|
})
|
||||||
|
|
||||||
|
function PlayerView() {
|
||||||
|
const { roomCode } = Route.useParams()
|
||||||
|
const { send } = useWebSocket(roomCode)
|
||||||
|
const { room, mySessionId, connectionStatus } = useRoomStore()
|
||||||
|
const joinSentRef = useRef(false)
|
||||||
|
const [manualName, setManualName] = useState("")
|
||||||
|
|
||||||
|
// Auto-send join_room when connected for the first time (no existing session)
|
||||||
|
useEffect(() => {
|
||||||
|
if (connectionStatus !== "connected" || mySessionId || joinSentRef.current) return
|
||||||
|
|
||||||
|
const displayName = sessionStorage.getItem("esc-party-join-name")
|
||||||
|
if (displayName) {
|
||||||
|
joinSentRef.current = true
|
||||||
|
sessionStorage.removeItem("esc-party-join-name")
|
||||||
|
send({ type: "join_room", displayName })
|
||||||
|
}
|
||||||
|
}, [connectionStatus, mySessionId, send])
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{connectionStatus === "connecting" ? "Connecting..." : "Room not found"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if no stored display name and no session (e.g., direct URL access),
|
||||||
|
// show a name input form
|
||||||
|
if (!mySessionId && connectionStatus === "connected" && !joinSentRef.current) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-4">
|
||||||
|
<h2 className="text-xl font-bold">Join Room {roomCode}</h2>
|
||||||
|
<Input
|
||||||
|
placeholder="Your name"
|
||||||
|
value={manualName}
|
||||||
|
onChange={(e) => setManualName(e.target.value)}
|
||||||
|
maxLength={20}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && manualName.trim()) {
|
||||||
|
joinSentRef.current = true
|
||||||
|
send({ type: "join_room", displayName: manualName.trim() })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (manualName.trim()) {
|
||||||
|
joinSentRef.current = true
|
||||||
|
send({ type: "join_room", displayName: manualName.trim() })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!manualName.trim()}
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mySessionId) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
|
<p className="text-muted-foreground">Joining room...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<RoomHeader roomCode={roomCode} currentAct={room.currentAct} connectionStatus={connectionStatus} />
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
{room.currentAct === "lobby" && (
|
||||||
|
<div className="flex flex-col items-center gap-4 py-8">
|
||||||
|
<p className="text-lg text-muted-foreground">Waiting for the host to start...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{room.currentAct === "ended" && (
|
||||||
|
<div className="flex flex-col items-center gap-4 py-8">
|
||||||
|
<p className="text-lg text-muted-foreground">The party has ended. Thanks for playing!</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Game UI will be added in later plans */}
|
||||||
|
<PlayerList players={room.players} mySessionId={mySessionId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
58
packages/client/src/stores/room-store.ts
Normal file
58
packages/client/src/stores/room-store.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { create } from "zustand"
|
||||||
|
import type { RoomState, Player } from "@celebrate-esc/shared"
|
||||||
|
|
||||||
|
interface RoomStore {
|
||||||
|
room: RoomState | null
|
||||||
|
mySessionId: string | null
|
||||||
|
connectionStatus: "disconnected" | "connecting" | "connected"
|
||||||
|
|
||||||
|
setRoom: (room: RoomState) => void
|
||||||
|
setMySessionId: (sessionId: string) => void
|
||||||
|
setConnectionStatus: (status: "disconnected" | "connecting" | "connected") => void
|
||||||
|
updatePlayerConnected: (playerId: string, connected: boolean) => void
|
||||||
|
addPlayer: (player: Player) => void
|
||||||
|
setAct: (act: RoomState["currentAct"]) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRoomStore = create<RoomStore>((set) => ({
|
||||||
|
room: null,
|
||||||
|
mySessionId: null,
|
||||||
|
connectionStatus: "disconnected",
|
||||||
|
|
||||||
|
setRoom: (room) => set({ room }),
|
||||||
|
setMySessionId: (sessionId) => set({ mySessionId: sessionId }),
|
||||||
|
setConnectionStatus: (status) => set({ connectionStatus: status }),
|
||||||
|
|
||||||
|
updatePlayerConnected: (playerId, connected) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.room) return state
|
||||||
|
return {
|
||||||
|
room: {
|
||||||
|
...state.room,
|
||||||
|
players: state.room.players.map((p) => (p.id === playerId ? { ...p, connected } : p)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
addPlayer: (player) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.room) return state
|
||||||
|
// Avoid duplicates
|
||||||
|
if (state.room.players.some((p) => p.id === player.id)) return state
|
||||||
|
return {
|
||||||
|
room: {
|
||||||
|
...state.room,
|
||||||
|
players: [...state.room.players, player],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
setAct: (act) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.room) return state
|
||||||
|
return { room: { ...state.room, currentAct: act } }
|
||||||
|
}),
|
||||||
|
|
||||||
|
reset: () => set({ room: null, mySessionId: null, connectionStatus: "disconnected" }),
|
||||||
|
}))
|
||||||
14
packages/client/tsconfig.json
Normal file
14
packages/client/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
23
packages/client/vite.config.ts
Normal file
23
packages/client/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [TanStackRouterVite(), react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:3001",
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
12
packages/server/data/scoring.json
Normal file
12
packages/server/data/scoring.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"prediction_winner": 25,
|
||||||
|
"prediction_top3": 10,
|
||||||
|
"prediction_nul_points": 15,
|
||||||
|
"jury_max_per_round": 5,
|
||||||
|
"bingo_per_square": 2,
|
||||||
|
"bingo_full_bonus": 10,
|
||||||
|
"quiz_easy": 5,
|
||||||
|
"quiz_medium": 10,
|
||||||
|
"quiz_hard": 15,
|
||||||
|
"dish_correct": 5
|
||||||
|
}
|
||||||
10
packages/server/drizzle.config.ts
Normal file
10
packages/server/drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./src/db/schema.ts",
|
||||||
|
out: "./drizzle",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
})
|
||||||
103
packages/server/drizzle/0000_new_phantom_reporter.sql
Normal file
103
packages/server/drizzle/0000_new_phantom_reporter.sql
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
CREATE TYPE "public"."act" AS ENUM('lobby', 'act1', 'act2', 'act3', 'ended');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."jury_round_status" AS ENUM('open', 'closed');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."quiz_round_status" AS ENUM('showing', 'buzzing', 'judging', 'resolved');--> statement-breakpoint
|
||||||
|
CREATE TABLE "bingo_cards" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"player_id" uuid NOT NULL,
|
||||||
|
"room_id" uuid NOT NULL,
|
||||||
|
"squares" jsonb NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "dish_guesses" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"player_id" uuid NOT NULL,
|
||||||
|
"dish_id" uuid NOT NULL,
|
||||||
|
"guessed_country" varchar NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "dishes" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"room_id" uuid NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL,
|
||||||
|
"correct_country" varchar NOT NULL,
|
||||||
|
"revealed" boolean DEFAULT false NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "jury_rounds" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"room_id" uuid NOT NULL,
|
||||||
|
"country_code" varchar NOT NULL,
|
||||||
|
"status" "jury_round_status" DEFAULT 'open' NOT NULL,
|
||||||
|
"opened_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "jury_votes" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"player_id" uuid NOT NULL,
|
||||||
|
"jury_round_id" uuid NOT NULL,
|
||||||
|
"rating" integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "players" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"room_id" uuid NOT NULL,
|
||||||
|
"session_id" uuid NOT NULL,
|
||||||
|
"display_name" varchar(20) NOT NULL,
|
||||||
|
"is_host" boolean DEFAULT false NOT NULL,
|
||||||
|
"connected" boolean DEFAULT false NOT NULL,
|
||||||
|
"joined_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "players_session_id_unique" UNIQUE("session_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "predictions" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"player_id" uuid NOT NULL,
|
||||||
|
"room_id" uuid NOT NULL,
|
||||||
|
"predicted_winner" varchar NOT NULL,
|
||||||
|
"top_3" jsonb NOT NULL,
|
||||||
|
"nul_points_pick" varchar NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "quiz_answers" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"player_id" uuid NOT NULL,
|
||||||
|
"quiz_round_id" uuid NOT NULL,
|
||||||
|
"buzzed_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"correct" boolean
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "quiz_rounds" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"room_id" uuid NOT NULL,
|
||||||
|
"question_id" varchar NOT NULL,
|
||||||
|
"status" "quiz_round_status" DEFAULT 'showing' NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "rooms" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"code" varchar(4) NOT NULL,
|
||||||
|
"current_act" "act" DEFAULT 'lobby' NOT NULL,
|
||||||
|
"host_session_id" uuid NOT NULL,
|
||||||
|
"actual_winner" varchar,
|
||||||
|
"actual_second" varchar,
|
||||||
|
"actual_third" varchar,
|
||||||
|
"actual_last" varchar,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"expires_at" timestamp NOT NULL,
|
||||||
|
CONSTRAINT "rooms_code_unique" UNIQUE("code")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "bingo_cards" ADD CONSTRAINT "bingo_cards_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "bingo_cards" ADD CONSTRAINT "bingo_cards_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "dish_guesses" ADD CONSTRAINT "dish_guesses_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "dish_guesses" ADD CONSTRAINT "dish_guesses_dish_id_dishes_id_fk" FOREIGN KEY ("dish_id") REFERENCES "public"."dishes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "dishes" ADD CONSTRAINT "dishes_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "jury_rounds" ADD CONSTRAINT "jury_rounds_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "jury_votes" ADD CONSTRAINT "jury_votes_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "jury_votes" ADD CONSTRAINT "jury_votes_jury_round_id_jury_rounds_id_fk" FOREIGN KEY ("jury_round_id") REFERENCES "public"."jury_rounds"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "players" ADD CONSTRAINT "players_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "predictions" ADD CONSTRAINT "predictions_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "predictions" ADD CONSTRAINT "predictions_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "quiz_answers" ADD CONSTRAINT "quiz_answers_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "quiz_answers" ADD CONSTRAINT "quiz_answers_quiz_round_id_quiz_rounds_id_fk" FOREIGN KEY ("quiz_round_id") REFERENCES "public"."quiz_rounds"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "quiz_rounds" ADD CONSTRAINT "quiz_rounds_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;
|
||||||
733
packages/server/drizzle/meta/0000_snapshot.json
Normal file
733
packages/server/drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
{
|
||||||
|
"id": "476634f4-fc77-43fc-9aaa-043d352b0b53",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.bingo_cards": {
|
||||||
|
"name": "bingo_cards",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"player_id": {
|
||||||
|
"name": "player_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"room_id": {
|
||||||
|
"name": "room_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"squares": {
|
||||||
|
"name": "squares",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"bingo_cards_player_id_players_id_fk": {
|
||||||
|
"name": "bingo_cards_player_id_players_id_fk",
|
||||||
|
"tableFrom": "bingo_cards",
|
||||||
|
"tableTo": "players",
|
||||||
|
"columnsFrom": [
|
||||||
|
"player_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"bingo_cards_room_id_rooms_id_fk": {
|
||||||
|
"name": "bingo_cards_room_id_rooms_id_fk",
|
||||||
|
"tableFrom": "bingo_cards",
|
||||||
|
"tableTo": "rooms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"room_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.dish_guesses": {
|
||||||
|
"name": "dish_guesses",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"player_id": {
|
||||||
|
"name": "player_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"dish_id": {
|
||||||
|
"name": "dish_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"guessed_country": {
|
||||||
|
"name": "guessed_country",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"dish_guesses_player_id_players_id_fk": {
|
||||||
|
"name": "dish_guesses_player_id_players_id_fk",
|
||||||
|
"tableFrom": "dish_guesses",
|
||||||
|
"tableTo": "players",
|
||||||
|
"columnsFrom": [
|
||||||
|
"player_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"dish_guesses_dish_id_dishes_id_fk": {
|
||||||
|
"name": "dish_guesses_dish_id_dishes_id_fk",
|
||||||
|
"tableFrom": "dish_guesses",
|
||||||
|
"tableTo": "dishes",
|
||||||
|
"columnsFrom": [
|
||||||
|
"dish_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.dishes": {
|
||||||
|
"name": "dishes",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"room_id": {
|
||||||
|
"name": "room_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"correct_country": {
|
||||||
|
"name": "correct_country",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"revealed": {
|
||||||
|
"name": "revealed",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"dishes_room_id_rooms_id_fk": {
|
||||||
|
"name": "dishes_room_id_rooms_id_fk",
|
||||||
|
"tableFrom": "dishes",
|
||||||
|
"tableTo": "rooms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"room_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.jury_rounds": {
|
||||||
|
"name": "jury_rounds",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"room_id": {
|
||||||
|
"name": "room_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "jury_round_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'open'"
|
||||||
|
},
|
||||||
|
"opened_at": {
|
||||||
|
"name": "opened_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"jury_rounds_room_id_rooms_id_fk": {
|
||||||
|
"name": "jury_rounds_room_id_rooms_id_fk",
|
||||||
|
"tableFrom": "jury_rounds",
|
||||||
|
"tableTo": "rooms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"room_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.jury_votes": {
|
||||||
|
"name": "jury_votes",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"player_id": {
|
||||||
|
"name": "player_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"jury_round_id": {
|
||||||
|
"name": "jury_round_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"jury_votes_player_id_players_id_fk": {
|
||||||
|
"name": "jury_votes_player_id_players_id_fk",
|
||||||
|
"tableFrom": "jury_votes",
|
||||||
|
"tableTo": "players",
|
||||||
|
"columnsFrom": [
|
||||||
|
"player_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"jury_votes_jury_round_id_jury_rounds_id_fk": {
|
||||||
|
"name": "jury_votes_jury_round_id_jury_rounds_id_fk",
|
||||||
|
"tableFrom": "jury_votes",
|
||||||
|
"tableTo": "jury_rounds",
|
||||||
|
"columnsFrom": [
|
||||||
|
"jury_round_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.players": {
|
||||||
|
"name": "players",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"room_id": {
|
||||||
|
"name": "room_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"name": "session_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"name": "display_name",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"is_host": {
|
||||||
|
"name": "is_host",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"name": "connected",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"joined_at": {
|
||||||
|
"name": "joined_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"players_room_id_rooms_id_fk": {
|
||||||
|
"name": "players_room_id_rooms_id_fk",
|
||||||
|
"tableFrom": "players",
|
||||||
|
"tableTo": "rooms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"room_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"players_session_id_unique": {
|
||||||
|
"name": "players_session_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.predictions": {
|
||||||
|
"name": "predictions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"player_id": {
|
||||||
|
"name": "player_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"room_id": {
|
||||||
|
"name": "room_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"predicted_winner": {
|
||||||
|
"name": "predicted_winner",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"top_3": {
|
||||||
|
"name": "top_3",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"nul_points_pick": {
|
||||||
|
"name": "nul_points_pick",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"predictions_player_id_players_id_fk": {
|
||||||
|
"name": "predictions_player_id_players_id_fk",
|
||||||
|
"tableFrom": "predictions",
|
||||||
|
"tableTo": "players",
|
||||||
|
"columnsFrom": [
|
||||||
|
"player_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"predictions_room_id_rooms_id_fk": {
|
||||||
|
"name": "predictions_room_id_rooms_id_fk",
|
||||||
|
"tableFrom": "predictions",
|
||||||
|
"tableTo": "rooms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"room_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.quiz_answers": {
|
||||||
|
"name": "quiz_answers",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"player_id": {
|
||||||
|
"name": "player_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"quiz_round_id": {
|
||||||
|
"name": "quiz_round_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"buzzed_at": {
|
||||||
|
"name": "buzzed_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"correct": {
|
||||||
|
"name": "correct",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"quiz_answers_player_id_players_id_fk": {
|
||||||
|
"name": "quiz_answers_player_id_players_id_fk",
|
||||||
|
"tableFrom": "quiz_answers",
|
||||||
|
"tableTo": "players",
|
||||||
|
"columnsFrom": [
|
||||||
|
"player_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"quiz_answers_quiz_round_id_quiz_rounds_id_fk": {
|
||||||
|
"name": "quiz_answers_quiz_round_id_quiz_rounds_id_fk",
|
||||||
|
"tableFrom": "quiz_answers",
|
||||||
|
"tableTo": "quiz_rounds",
|
||||||
|
"columnsFrom": [
|
||||||
|
"quiz_round_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.quiz_rounds": {
|
||||||
|
"name": "quiz_rounds",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"room_id": {
|
||||||
|
"name": "room_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"question_id": {
|
||||||
|
"name": "question_id",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "quiz_round_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'showing'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"quiz_rounds_room_id_rooms_id_fk": {
|
||||||
|
"name": "quiz_rounds_room_id_rooms_id_fk",
|
||||||
|
"tableFrom": "quiz_rounds",
|
||||||
|
"tableTo": "rooms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"room_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.rooms": {
|
||||||
|
"name": "rooms",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"name": "code",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"current_act": {
|
||||||
|
"name": "current_act",
|
||||||
|
"type": "act",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'lobby'"
|
||||||
|
},
|
||||||
|
"host_session_id": {
|
||||||
|
"name": "host_session_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"actual_winner": {
|
||||||
|
"name": "actual_winner",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"actual_second": {
|
||||||
|
"name": "actual_second",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"actual_third": {
|
||||||
|
"name": "actual_third",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"actual_last": {
|
||||||
|
"name": "actual_last",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"rooms_code_unique": {
|
||||||
|
"name": "rooms_code_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.act": {
|
||||||
|
"name": "act",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"lobby",
|
||||||
|
"act1",
|
||||||
|
"act2",
|
||||||
|
"act3",
|
||||||
|
"ended"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.jury_round_status": {
|
||||||
|
"name": "jury_round_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"open",
|
||||||
|
"closed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.quiz_round_status": {
|
||||||
|
"name": "quiz_round_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"showing",
|
||||||
|
"buzzing",
|
||||||
|
"judging",
|
||||||
|
"resolved"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/server/drizzle/meta/_journal.json
Normal file
13
packages/server/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773228528975,
|
||||||
|
"tag": "0000_new_phantom_reporter",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
30
packages/server/package.json
Normal file
30
packages/server/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@celebrate-esc/server",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --env-file=../../.env --watch src/index.ts",
|
||||||
|
"build": "tsc --noEmit",
|
||||||
|
"start": "node --experimental-strip-types src/index.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@celebrate-esc/shared": "workspace:*",
|
||||||
|
"@hono/node-server": "latest",
|
||||||
|
"@hono/node-ws": "latest",
|
||||||
|
"@hono/zod-validator": "latest",
|
||||||
|
"drizzle-orm": "latest",
|
||||||
|
"hono": "latest",
|
||||||
|
"pg": "latest",
|
||||||
|
"zod": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "latest",
|
||||||
|
"drizzle-kit": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
"vitest": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/server/src/app.ts
Normal file
26
packages/server/src/app.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Hono } from "hono"
|
||||||
|
import { cors } from "hono/cors"
|
||||||
|
import { createNodeWebSocket } from "@hono/node-ws"
|
||||||
|
import { zValidator } from "@hono/zod-validator"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { roomManager } from "./rooms/index"
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.use("*", cors())
|
||||||
|
|
||||||
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
|
||||||
|
|
||||||
|
app.get("/health", (c) => c.json({ status: "ok" }))
|
||||||
|
|
||||||
|
const createRoomSchema = z.object({
|
||||||
|
displayName: z.string().min(1).max(20),
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post("/rooms", zValidator("json", createRoomSchema), (c) => {
|
||||||
|
const { displayName } = c.req.valid("json")
|
||||||
|
const result = roomManager.createRoom(displayName)
|
||||||
|
return c.json({ data: result })
|
||||||
|
})
|
||||||
|
|
||||||
|
export { app, injectWebSocket, upgradeWebSocket }
|
||||||
8
packages/server/src/db/client.ts
Normal file
8
packages/server/src/db/client.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/node-postgres"
|
||||||
|
import * as schema from "./schema"
|
||||||
|
|
||||||
|
export function createDb(databaseUrl: string) {
|
||||||
|
return drizzle(databaseUrl, { schema })
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Database = ReturnType<typeof createDb>
|
||||||
129
packages/server/src/db/schema.ts
Normal file
129
packages/server/src/db/schema.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { boolean, integer, jsonb, pgEnum, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const actEnum = pgEnum("act", ["lobby", "act1", "act2", "act3", "ended"])
|
||||||
|
export const juryRoundStatusEnum = pgEnum("jury_round_status", ["open", "closed"])
|
||||||
|
export const quizRoundStatusEnum = pgEnum("quiz_round_status", ["showing", "buzzing", "judging", "resolved"])
|
||||||
|
|
||||||
|
// ─── Room System ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const rooms = pgTable("rooms", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
code: varchar("code", { length: 4 }).notNull().unique(),
|
||||||
|
currentAct: actEnum("current_act").notNull().default("lobby"),
|
||||||
|
hostSessionId: uuid("host_session_id").notNull(),
|
||||||
|
actualWinner: varchar("actual_winner"),
|
||||||
|
actualSecond: varchar("actual_second"),
|
||||||
|
actualThird: varchar("actual_third"),
|
||||||
|
actualLast: varchar("actual_last"),
|
||||||
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const players = pgTable("players", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => rooms.id),
|
||||||
|
sessionId: uuid("session_id").notNull().unique(),
|
||||||
|
displayName: varchar("display_name", { length: 20 }).notNull(),
|
||||||
|
isHost: boolean("is_host").notNull().default(false),
|
||||||
|
connected: boolean("connected").notNull().default(false),
|
||||||
|
joinedAt: timestamp("joined_at").notNull().defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Predictions (Plan 2) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export const predictions = pgTable("predictions", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
playerId: uuid("player_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => players.id),
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => rooms.id),
|
||||||
|
predictedWinner: varchar("predicted_winner").notNull(),
|
||||||
|
top3: jsonb("top_3").notNull().$type<string[]>(),
|
||||||
|
nulPointsPick: varchar("nul_points_pick").notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Jury Voting (Plan 3) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export const juryRounds = pgTable("jury_rounds", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => rooms.id),
|
||||||
|
countryCode: varchar("country_code").notNull(),
|
||||||
|
status: juryRoundStatusEnum("status").notNull().default("open"),
|
||||||
|
openedAt: timestamp("opened_at").notNull().defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const juryVotes = pgTable("jury_votes", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
playerId: uuid("player_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => players.id),
|
||||||
|
juryRoundId: uuid("jury_round_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => juryRounds.id),
|
||||||
|
rating: integer("rating").notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Bingo (Plan 3) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const bingoCards = pgTable("bingo_cards", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
playerId: uuid("player_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => players.id),
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => rooms.id),
|
||||||
|
squares: jsonb("squares").notNull().$type<{ tropeId: string; tapped: boolean }[]>(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Dishes (Plan 2) ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const dishes = pgTable("dishes", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => rooms.id),
|
||||||
|
name: varchar("name", { length: 100 }).notNull(),
|
||||||
|
correctCountry: varchar("correct_country").notNull(),
|
||||||
|
revealed: boolean("revealed").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const dishGuesses = pgTable("dish_guesses", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
playerId: uuid("player_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => players.id),
|
||||||
|
dishId: uuid("dish_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => dishes.id),
|
||||||
|
guessedCountry: varchar("guessed_country").notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Quiz (Plan 4) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const quizRounds = pgTable("quiz_rounds", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
roomId: uuid("room_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => rooms.id),
|
||||||
|
questionId: varchar("question_id").notNull(),
|
||||||
|
status: quizRoundStatusEnum("status").notNull().default("showing"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const quizAnswers = pgTable("quiz_answers", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
playerId: uuid("player_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => players.id),
|
||||||
|
quizRoundId: uuid("quiz_round_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => quizRounds.id),
|
||||||
|
buzzedAt: timestamp("buzzed_at").notNull().defaultNow(),
|
||||||
|
correct: boolean("correct"),
|
||||||
|
})
|
||||||
8
packages/server/src/env.ts
Normal file
8
packages/server/src/env.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
const envSchema = z.object({
|
||||||
|
DATABASE_URL: z.string().url(),
|
||||||
|
PORT: z.coerce.number().default(3001),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const env = envSchema.parse(process.env)
|
||||||
12
packages/server/src/index.ts
Normal file
12
packages/server/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { serve } from "@hono/node-server"
|
||||||
|
import { app, injectWebSocket } from "./app"
|
||||||
|
import { registerWebSocketRoutes } from "./ws/handler"
|
||||||
|
import { env } from "./env"
|
||||||
|
|
||||||
|
registerWebSocketRoutes()
|
||||||
|
|
||||||
|
const server = serve({ fetch: app.fetch, port: env.PORT }, (info) => {
|
||||||
|
console.log(`celebrate-esc server running on http://localhost:${info.port}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
injectWebSocket(server)
|
||||||
3
packages/server/src/rooms/index.ts
Normal file
3
packages/server/src/rooms/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { RoomManager } from "./room-manager"
|
||||||
|
|
||||||
|
export const roomManager = new RoomManager()
|
||||||
158
packages/server/src/rooms/room-manager.ts
Normal file
158
packages/server/src/rooms/room-manager.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { randomUUID } from "node:crypto"
|
||||||
|
import { ACTS, MAX_PLAYERS, ROOM_CODE_CHARS, ROOM_CODE_LENGTH, ROOM_EXPIRY_HOURS } from "@celebrate-esc/shared"
|
||||||
|
import type { Act, RoomState } from "@celebrate-esc/shared"
|
||||||
|
|
||||||
|
interface InternalPlayer {
|
||||||
|
id: string
|
||||||
|
sessionId: string
|
||||||
|
displayName: string
|
||||||
|
isHost: boolean
|
||||||
|
connected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InternalRoom {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
currentAct: Act
|
||||||
|
hostSessionId: string
|
||||||
|
players: Map<string, InternalPlayer> // sessionId -> player
|
||||||
|
createdAt: Date
|
||||||
|
expiresAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoomManager {
|
||||||
|
private rooms = new Map<string, InternalRoom>()
|
||||||
|
|
||||||
|
createRoom(hostDisplayName: string): { code: string; sessionId: string } {
|
||||||
|
const code = this.generateRoomCode()
|
||||||
|
const sessionId = randomUUID()
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const host: InternalPlayer = {
|
||||||
|
id: randomUUID(),
|
||||||
|
sessionId,
|
||||||
|
displayName: hostDisplayName,
|
||||||
|
isHost: true,
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const room: InternalRoom = {
|
||||||
|
id: randomUUID(),
|
||||||
|
code,
|
||||||
|
currentAct: "lobby",
|
||||||
|
hostSessionId: sessionId,
|
||||||
|
players: new Map([[sessionId, host]]),
|
||||||
|
createdAt: now,
|
||||||
|
expiresAt: new Date(now.getTime() + ROOM_EXPIRY_HOURS * 60 * 60 * 1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rooms.set(code, room)
|
||||||
|
return { code, sessionId }
|
||||||
|
}
|
||||||
|
|
||||||
|
joinRoom(code: string, displayName: string): { sessionId: string } | { error: string } {
|
||||||
|
const room = this.rooms.get(code)
|
||||||
|
if (!room) return { error: "Room not found" }
|
||||||
|
if (room.currentAct === "ended") return { error: "Room has ended" }
|
||||||
|
if (room.players.size >= MAX_PLAYERS) return { error: "Room is full" }
|
||||||
|
|
||||||
|
for (const player of room.players.values()) {
|
||||||
|
if (player.displayName.toLowerCase() === displayName.toLowerCase()) return { error: "Name already taken" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = randomUUID()
|
||||||
|
room.players.set(sessionId, {
|
||||||
|
id: randomUUID(),
|
||||||
|
sessionId,
|
||||||
|
displayName,
|
||||||
|
isHost: false,
|
||||||
|
connected: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { sessionId }
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceAct(code: string, sessionId: string): { newAct: Act } | { error: string } {
|
||||||
|
const room = this.rooms.get(code)
|
||||||
|
if (!room) return { error: "Room not found" }
|
||||||
|
if (room.hostSessionId !== sessionId) return { error: "Only the host can advance acts" }
|
||||||
|
if (room.currentAct === "ended") return { error: "Room has already ended" }
|
||||||
|
|
||||||
|
const currentIndex = ACTS.indexOf(room.currentAct)
|
||||||
|
const nextAct = ACTS[currentIndex + 1]!
|
||||||
|
room.currentAct = nextAct
|
||||||
|
return { newAct: nextAct }
|
||||||
|
}
|
||||||
|
|
||||||
|
endRoom(code: string, sessionId: string): { success: true } | { error: string } {
|
||||||
|
const room = this.rooms.get(code)
|
||||||
|
if (!room) return { error: "Room not found" }
|
||||||
|
if (room.hostSessionId !== sessionId) return { error: "Only the host can end the room" }
|
||||||
|
|
||||||
|
room.currentAct = "ended"
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectPlayer(code: string, sessionId: string): { success: true; playerId: string } | { error: string } {
|
||||||
|
const room = this.rooms.get(code)
|
||||||
|
if (!room) return { error: "Room not found" }
|
||||||
|
|
||||||
|
const player = room.players.get(sessionId)
|
||||||
|
if (!player) return { error: "Session not found in this room" }
|
||||||
|
|
||||||
|
player.connected = true
|
||||||
|
return { success: true, playerId: player.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlayerConnected(code: string, sessionId: string, connected: boolean): void {
|
||||||
|
const room = this.rooms.get(code)
|
||||||
|
if (!room) return
|
||||||
|
|
||||||
|
const player = room.players.get(sessionId)
|
||||||
|
if (player) {
|
||||||
|
player.connected = connected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoom(code: string): RoomState | null {
|
||||||
|
const room = this.rooms.get(code)
|
||||||
|
if (!room) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: room.id,
|
||||||
|
code: room.code,
|
||||||
|
currentAct: room.currentAct,
|
||||||
|
hostSessionId: room.hostSessionId,
|
||||||
|
players: Array.from(room.players.values()).map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
sessionId: p.sessionId,
|
||||||
|
displayName: p.displayName,
|
||||||
|
isHost: p.isHost,
|
||||||
|
connected: p.connected,
|
||||||
|
})),
|
||||||
|
createdAt: room.createdAt.toISOString(),
|
||||||
|
expiresAt: room.expiresAt.toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHost(code: string, sessionId: string): boolean {
|
||||||
|
const room = this.rooms.get(code)
|
||||||
|
return room?.hostSessionId === sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear all rooms -- used in tests */
|
||||||
|
reset(): void {
|
||||||
|
this.rooms.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRoomCode(): string {
|
||||||
|
let code: string
|
||||||
|
do {
|
||||||
|
code = Array.from(
|
||||||
|
{ length: ROOM_CODE_LENGTH },
|
||||||
|
() => ROOM_CODE_CHARS[Math.floor(Math.random() * ROOM_CODE_CHARS.length)]!,
|
||||||
|
).join("")
|
||||||
|
} while (this.rooms.has(code))
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
||||||
66
packages/server/src/rooms/room-service.ts
Normal file
66
packages/server/src/rooms/room-service.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { eq } from "drizzle-orm"
|
||||||
|
import type { Database } from "../db/client"
|
||||||
|
import { players, rooms } from "../db/schema"
|
||||||
|
|
||||||
|
export class RoomService {
|
||||||
|
constructor(private db: Database) {}
|
||||||
|
|
||||||
|
async persistRoom(room: {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
currentAct: string
|
||||||
|
hostSessionId: string
|
||||||
|
expiresAt: Date
|
||||||
|
}) {
|
||||||
|
await this.db
|
||||||
|
.insert(rooms)
|
||||||
|
.values({
|
||||||
|
id: room.id,
|
||||||
|
code: room.code,
|
||||||
|
currentAct: room.currentAct as "lobby" | "act1" | "act2" | "act3" | "ended",
|
||||||
|
hostSessionId: room.hostSessionId,
|
||||||
|
expiresAt: room.expiresAt,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: rooms.id,
|
||||||
|
set: {
|
||||||
|
currentAct: room.currentAct as "lobby" | "act1" | "act2" | "act3" | "ended",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async persistPlayer(player: {
|
||||||
|
id: string
|
||||||
|
roomId: string
|
||||||
|
sessionId: string
|
||||||
|
displayName: string
|
||||||
|
isHost: boolean
|
||||||
|
}) {
|
||||||
|
await this.db
|
||||||
|
.insert(players)
|
||||||
|
.values({
|
||||||
|
id: player.id,
|
||||||
|
roomId: player.roomId,
|
||||||
|
sessionId: player.sessionId,
|
||||||
|
displayName: player.displayName,
|
||||||
|
isHost: player.isHost,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: players.id,
|
||||||
|
set: {
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRoomAct(roomId: string, act: string) {
|
||||||
|
await this.db
|
||||||
|
.update(rooms)
|
||||||
|
.set({ currentAct: act as "lobby" | "act1" | "act2" | "act3" | "ended" })
|
||||||
|
.where(eq(rooms.id, roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePlayerConnected(sessionId: string, connected: boolean) {
|
||||||
|
await this.db.update(players).set({ connected }).where(eq(players.sessionId, sessionId))
|
||||||
|
}
|
||||||
|
}
|
||||||
219
packages/server/src/ws/handler.ts
Normal file
219
packages/server/src/ws/handler.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import type { WSContext } from "hono/ws"
|
||||||
|
import { clientMessage } from "@celebrate-esc/shared"
|
||||||
|
import type { ServerMessage } from "@celebrate-esc/shared"
|
||||||
|
import { app, upgradeWebSocket } from "../app"
|
||||||
|
import { roomManager } from "../rooms/index"
|
||||||
|
|
||||||
|
// Track all WebSocket connections per room
|
||||||
|
interface Connection {
|
||||||
|
ws: WSContext
|
||||||
|
sessionId: string | null
|
||||||
|
}
|
||||||
|
const roomConnections = new Map<string, Set<Connection>>()
|
||||||
|
|
||||||
|
function getConnections(roomCode: string): Set<Connection> {
|
||||||
|
let conns = roomConnections.get(roomCode)
|
||||||
|
if (!conns) {
|
||||||
|
conns = new Set()
|
||||||
|
roomConnections.set(roomCode, conns)
|
||||||
|
}
|
||||||
|
return conns
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcast(roomCode: string, message: ServerMessage) {
|
||||||
|
const data = JSON.stringify(message)
|
||||||
|
const conns = roomConnections.get(roomCode)
|
||||||
|
if (!conns) return
|
||||||
|
for (const conn of conns) {
|
||||||
|
try {
|
||||||
|
conn.ws.send(data)
|
||||||
|
} catch {
|
||||||
|
// Connection may be closed -- will be cleaned up on onClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTo(ws: WSContext, message: ServerMessage) {
|
||||||
|
ws.send(JSON.stringify(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendError(ws: WSContext, message: string) {
|
||||||
|
sendTo(ws, { type: "error", message })
|
||||||
|
}
|
||||||
|
|
||||||
|
let registered = false
|
||||||
|
|
||||||
|
export function registerWebSocketRoutes() {
|
||||||
|
if (registered) return
|
||||||
|
registered = true
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
"/ws/:roomCode",
|
||||||
|
upgradeWebSocket((c) => {
|
||||||
|
const roomCode = c.req.param("roomCode")!
|
||||||
|
let sessionId: string | null = c.req.query("sessionId") ?? null
|
||||||
|
let connection: Connection | null = null
|
||||||
|
|
||||||
|
return {
|
||||||
|
onOpen(_event, ws) {
|
||||||
|
const room = roomManager.getRoom(roomCode)
|
||||||
|
if (!room) {
|
||||||
|
sendError(ws, "Room not found")
|
||||||
|
ws.close(4004, "Room not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connection = { ws, sessionId }
|
||||||
|
getConnections(roomCode).add(connection)
|
||||||
|
|
||||||
|
// If sessionId provided, attempt reconnect
|
||||||
|
if (sessionId) {
|
||||||
|
const result = roomManager.reconnectPlayer(roomCode, sessionId)
|
||||||
|
if ("error" in result) {
|
||||||
|
sendError(ws, result.error)
|
||||||
|
sessionId = null
|
||||||
|
connection.sessionId = null
|
||||||
|
} else {
|
||||||
|
roomManager.setPlayerConnected(roomCode, sessionId, true)
|
||||||
|
sendTo(ws, {
|
||||||
|
type: "room_state",
|
||||||
|
room: roomManager.getRoom(roomCode)!,
|
||||||
|
})
|
||||||
|
broadcast(roomCode, {
|
||||||
|
type: "player_reconnected",
|
||||||
|
playerId: result.playerId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Passive viewer (display) or player about to send join_room
|
||||||
|
sendTo(ws, {
|
||||||
|
type: "room_state",
|
||||||
|
room: roomManager.getRoom(roomCode)!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMessage(event, ws) {
|
||||||
|
let data: unknown
|
||||||
|
try {
|
||||||
|
data = JSON.parse(typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data as ArrayBuffer))
|
||||||
|
} catch {
|
||||||
|
sendError(ws, "Invalid JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = clientMessage.safeParse(data)
|
||||||
|
if (!parsed.success) {
|
||||||
|
sendError(ws, `Invalid message: ${parsed.error.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = parsed.data
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case "join_room": {
|
||||||
|
if (sessionId) {
|
||||||
|
sendError(ws, "Already joined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = roomManager.joinRoom(roomCode, msg.displayName)
|
||||||
|
if ("error" in result) {
|
||||||
|
sendError(ws, result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionId = result.sessionId
|
||||||
|
if (connection) connection.sessionId = sessionId
|
||||||
|
roomManager.setPlayerConnected(roomCode, sessionId, true)
|
||||||
|
|
||||||
|
// Send room state with session ID to the new player
|
||||||
|
sendTo(ws, {
|
||||||
|
type: "room_state",
|
||||||
|
room: roomManager.getRoom(roomCode)!,
|
||||||
|
sessionId: result.sessionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Broadcast player joined to everyone
|
||||||
|
const room = roomManager.getRoom(roomCode)!
|
||||||
|
const newPlayer = room.players.find((p) => p.sessionId === sessionId)!
|
||||||
|
broadcast(roomCode, {
|
||||||
|
type: "player_joined",
|
||||||
|
player: newPlayer,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "reconnect": {
|
||||||
|
const result = roomManager.reconnectPlayer(roomCode, msg.sessionId)
|
||||||
|
if ("error" in result) {
|
||||||
|
sendError(ws, result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionId = msg.sessionId
|
||||||
|
if (connection) connection.sessionId = sessionId
|
||||||
|
roomManager.setPlayerConnected(roomCode, sessionId, true)
|
||||||
|
sendTo(ws, {
|
||||||
|
type: "room_state",
|
||||||
|
room: roomManager.getRoom(roomCode)!,
|
||||||
|
})
|
||||||
|
broadcast(roomCode, {
|
||||||
|
type: "player_reconnected",
|
||||||
|
playerId: result.playerId,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "advance_act": {
|
||||||
|
if (!sessionId) {
|
||||||
|
sendError(ws, "Not joined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = roomManager.advanceAct(roomCode, sessionId)
|
||||||
|
if ("error" in result) {
|
||||||
|
sendError(ws, result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
broadcast(roomCode, {
|
||||||
|
type: "act_changed",
|
||||||
|
newAct: result.newAct,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "end_room": {
|
||||||
|
if (!sessionId) {
|
||||||
|
sendError(ws, "Not joined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = roomManager.endRoom(roomCode, sessionId)
|
||||||
|
if ("error" in result) {
|
||||||
|
sendError(ws, result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
broadcast(roomCode, { type: "room_ended" })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
if (connection) {
|
||||||
|
getConnections(roomCode).delete(connection)
|
||||||
|
}
|
||||||
|
if (sessionId) {
|
||||||
|
roomManager.setPlayerConnected(roomCode, sessionId, false)
|
||||||
|
const room = roomManager.getRoom(roomCode)
|
||||||
|
if (room) {
|
||||||
|
const player = room.players.find((p) => p.sessionId === sessionId)
|
||||||
|
if (player) {
|
||||||
|
broadcast(roomCode, {
|
||||||
|
type: "player_disconnected",
|
||||||
|
playerId: player.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
175
packages/server/tests/room-manager.test.ts
Normal file
175
packages/server/tests/room-manager.test.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { describe, expect, it, beforeEach } from "vitest"
|
||||||
|
import { RoomManager } from "../src/rooms/room-manager"
|
||||||
|
import type { Act } from "@celebrate-esc/shared"
|
||||||
|
|
||||||
|
describe("RoomManager", () => {
|
||||||
|
let manager: RoomManager
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new RoomManager()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("createRoom", () => {
|
||||||
|
it("returns a 4-character room code and session ID", () => {
|
||||||
|
const result = manager.createRoom("Host")
|
||||||
|
expect(result.code).toMatch(/^[A-Z0-9]{4}$/)
|
||||||
|
expect(result.sessionId).toBeDefined()
|
||||||
|
expect(result.sessionId.length).toBe(36) // UUID
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates the host as a player in the room", () => {
|
||||||
|
const { code, sessionId } = manager.createRoom("Host")
|
||||||
|
const room = manager.getRoom(code)
|
||||||
|
expect(room).toBeDefined()
|
||||||
|
expect(room!.players).toHaveLength(1)
|
||||||
|
expect(room!.players[0]!.displayName).toBe("Host")
|
||||||
|
expect(room!.players[0]!.isHost).toBe(true)
|
||||||
|
expect(room!.players[0]!.sessionId).toBe(sessionId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("starts in lobby state", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const room = manager.getRoom(code)
|
||||||
|
expect(room!.currentAct).toBe("lobby")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates unique room codes", () => {
|
||||||
|
const codes = new Set<string>()
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const { code } = manager.createRoom(`Host ${i}`)
|
||||||
|
codes.add(code)
|
||||||
|
}
|
||||||
|
expect(codes.size).toBe(50)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("joinRoom", () => {
|
||||||
|
it("adds a player to an existing room", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const result = manager.joinRoom(code, "Player 1")
|
||||||
|
|
||||||
|
expect("sessionId" in result).toBe(true)
|
||||||
|
if ("sessionId" in result) {
|
||||||
|
const room = manager.getRoom(code)
|
||||||
|
expect(room!.players).toHaveLength(2)
|
||||||
|
expect(room!.players[1]!.displayName).toBe("Player 1")
|
||||||
|
expect(room!.players[1]!.isHost).toBe(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects join if room not found", () => {
|
||||||
|
const result = manager.joinRoom("ZZZZ", "Player")
|
||||||
|
expect(result).toEqual({ error: "Room not found" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects join if room has ended", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
// Force room to ended state
|
||||||
|
manager.advanceAct(code, manager.getRoom(code)!.hostSessionId)
|
||||||
|
manager.advanceAct(code, manager.getRoom(code)!.hostSessionId)
|
||||||
|
manager.advanceAct(code, manager.getRoom(code)!.hostSessionId)
|
||||||
|
manager.advanceAct(code, manager.getRoom(code)!.hostSessionId)
|
||||||
|
const result = manager.joinRoom(code, "Late Player")
|
||||||
|
expect(result).toEqual({ error: "Room has ended" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects join if display name is taken", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
manager.joinRoom(code, "Player 1")
|
||||||
|
const result = manager.joinRoom(code, "Player 1")
|
||||||
|
expect(result).toEqual({ error: "Name already taken" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects join if room is full (10 players)", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
for (let i = 1; i <= 9; i++) {
|
||||||
|
manager.joinRoom(code, `Player ${i}`)
|
||||||
|
}
|
||||||
|
const result = manager.joinRoom(code, "Player 10")
|
||||||
|
expect(result).toEqual({ error: "Room is full" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("advanceAct", () => {
|
||||||
|
it("advances through acts in order", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const room = manager.getRoom(code)!
|
||||||
|
const hostSession = room.hostSessionId
|
||||||
|
|
||||||
|
const expectedSequence: Act[] = ["act1", "act2", "act3", "ended"]
|
||||||
|
for (const expected of expectedSequence) {
|
||||||
|
const result = manager.advanceAct(code, hostSession)
|
||||||
|
expect(result).toEqual({ newAct: expected })
|
||||||
|
expect(manager.getRoom(code)!.currentAct).toBe(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("cannot advance past ended", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const room = manager.getRoom(code)!
|
||||||
|
// Advance to ended
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
manager.advanceAct(code, room.hostSessionId)
|
||||||
|
}
|
||||||
|
const result = manager.advanceAct(code, room.hostSessionId)
|
||||||
|
expect(result).toEqual({ error: "Room has already ended" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects advance from non-host", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const joinResult = manager.joinRoom(code, "Player")
|
||||||
|
if ("sessionId" in joinResult) {
|
||||||
|
const result = manager.advanceAct(code, joinResult.sessionId)
|
||||||
|
expect(result).toEqual({ error: "Only the host can advance acts" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("endRoom", () => {
|
||||||
|
it("sets room to ended state", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const room = manager.getRoom(code)!
|
||||||
|
const result = manager.endRoom(code, room.hostSessionId)
|
||||||
|
expect(result).toEqual({ success: true })
|
||||||
|
expect(manager.getRoom(code)!.currentAct).toBe("ended")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects end from non-host", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const joinResult = manager.joinRoom(code, "Player")
|
||||||
|
if ("sessionId" in joinResult) {
|
||||||
|
const result = manager.endRoom(code, joinResult.sessionId)
|
||||||
|
expect(result).toEqual({ error: "Only the host can end the room" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getRoom", () => {
|
||||||
|
it("returns null for non-existent room", () => {
|
||||||
|
expect(manager.getRoom("ZZZZ")).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns serialized room state", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const room = manager.getRoom(code)
|
||||||
|
expect(room).toBeDefined()
|
||||||
|
expect(room!.code).toBe(code)
|
||||||
|
expect(room!.currentAct).toBe("lobby")
|
||||||
|
expect(Array.isArray(room!.players)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("reconnect", () => {
|
||||||
|
it("re-identifies an existing player by session ID", () => {
|
||||||
|
const { code, sessionId } = manager.createRoom("Host")
|
||||||
|
const result = manager.reconnectPlayer(code, sessionId)
|
||||||
|
expect(result).toEqual({ success: true, playerId: expect.any(String) })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects reconnect with unknown session ID", () => {
|
||||||
|
const { code } = manager.createRoom("Host")
|
||||||
|
const result = manager.reconnectPlayer(code, "00000000-0000-0000-0000-000000000000")
|
||||||
|
expect(result).toEqual({ error: "Session not found in this room" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
108
packages/server/tests/ws-handler.test.ts
Normal file
108
packages/server/tests/ws-handler.test.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, expect, it, afterEach, beforeEach } from "vitest"
|
||||||
|
import { serve } from "@hono/node-server"
|
||||||
|
import { app, injectWebSocket } from "../src/app"
|
||||||
|
import { registerWebSocketRoutes } from "../src/ws/handler"
|
||||||
|
import { roomManager } from "../src/rooms/index"
|
||||||
|
|
||||||
|
// Register WS routes once
|
||||||
|
registerWebSocketRoutes()
|
||||||
|
|
||||||
|
let server: ReturnType<typeof serve>
|
||||||
|
|
||||||
|
function waitForMessage(ws: WebSocket): Promise<unknown> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
ws.addEventListener(
|
||||||
|
"message",
|
||||||
|
(event) => {
|
||||||
|
resolve(JSON.parse(event.data as string))
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForOpen(ws: WebSocket): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
ws.addEventListener("open", () => resolve(), { once: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("WebSocket handler", () => {
|
||||||
|
let port: number
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
roomManager.reset()
|
||||||
|
port = 3100 + Math.floor(Math.random() * 900)
|
||||||
|
server = serve({ fetch: app.fetch, port })
|
||||||
|
injectWebSocket(server)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
server.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates a room via HTTP and connects via WebSocket", async () => {
|
||||||
|
// Create room via HTTP
|
||||||
|
const res = await fetch(`http://localhost:${port}/rooms`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ displayName: "Host" }),
|
||||||
|
})
|
||||||
|
const { data } = (await res.json()) as { data: { code: string; sessionId: string } }
|
||||||
|
expect(data.code).toMatch(/^[A-Z0-9]{4}$/)
|
||||||
|
|
||||||
|
// Connect as host via WebSocket
|
||||||
|
const ws = new WebSocket(`ws://localhost:${port}/ws/${data.code}?sessionId=${data.sessionId}`)
|
||||||
|
await waitForOpen(ws)
|
||||||
|
|
||||||
|
const msg = (await waitForMessage(ws)) as { type: string; room: { code: string } }
|
||||||
|
expect(msg.type).toBe("room_state")
|
||||||
|
expect(msg.room.code).toBe(data.code)
|
||||||
|
|
||||||
|
ws.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("player joins room via WebSocket", async () => {
|
||||||
|
// Create room
|
||||||
|
const res = await fetch(`http://localhost:${port}/rooms`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ displayName: "Host" }),
|
||||||
|
})
|
||||||
|
const { data } = (await res.json()) as { data: { code: string; sessionId: string } }
|
||||||
|
|
||||||
|
// Connect host
|
||||||
|
const hostWs = new WebSocket(`ws://localhost:${port}/ws/${data.code}?sessionId=${data.sessionId}`)
|
||||||
|
await waitForOpen(hostWs)
|
||||||
|
await waitForMessage(hostWs) // room_state
|
||||||
|
|
||||||
|
// Connect player (no sessionId)
|
||||||
|
const playerWs = new WebSocket(`ws://localhost:${port}/ws/${data.code}`)
|
||||||
|
await waitForOpen(playerWs)
|
||||||
|
await waitForMessage(playerWs) // initial room_state
|
||||||
|
|
||||||
|
// Set up listeners BEFORE sending to avoid race conditions
|
||||||
|
const playerMsgPromise = waitForMessage(playerWs)
|
||||||
|
const hostMsgPromise = waitForMessage(hostWs)
|
||||||
|
|
||||||
|
// Player sends join_room
|
||||||
|
playerWs.send(JSON.stringify({ type: "join_room", displayName: "Player 1" }))
|
||||||
|
|
||||||
|
// Player receives room_state with sessionId
|
||||||
|
const playerMsg = (await playerMsgPromise) as { type: string; sessionId?: string }
|
||||||
|
expect(playerMsg.type).toBe("room_state")
|
||||||
|
expect(playerMsg.sessionId).toBeDefined()
|
||||||
|
|
||||||
|
// Host receives player_joined broadcast
|
||||||
|
const hostMsg = (await hostMsgPromise) as { type: string; player: { displayName: string } }
|
||||||
|
expect(hostMsg.type).toBe("player_joined")
|
||||||
|
expect(hostMsg.player.displayName).toBe("Player 1")
|
||||||
|
|
||||||
|
hostWs.close()
|
||||||
|
playerWs.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
8
packages/server/tsconfig.json
Normal file
8
packages/server/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
18
packages/shared/package.json
Normal file
18
packages/shared/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@celebrate-esc/shared",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --noEmit",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/shared/src/constants.ts
Normal file
17
packages/shared/src/constants.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const MAX_PLAYERS = 10
|
||||||
|
export const ROOM_CODE_LENGTH = 4
|
||||||
|
export const ROOM_EXPIRY_HOURS = 12
|
||||||
|
|
||||||
|
/** Characters used for room codes — excludes I/O/0/1 to avoid confusion */
|
||||||
|
export const ROOM_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
|
|
||||||
|
export const ACTS = ["lobby", "act1", "act2", "act3", "ended"] as const
|
||||||
|
export type Act = (typeof ACTS)[number]
|
||||||
|
|
||||||
|
/** Rating range for jury voting (Eurovision convention: 1-12) */
|
||||||
|
export const JURY_RATING_MIN = 1
|
||||||
|
export const JURY_RATING_MAX = 12
|
||||||
|
|
||||||
|
/** Bingo grid dimensions */
|
||||||
|
export const BINGO_GRID_SIZE = 4
|
||||||
|
export const BINGO_TOTAL_SQUARES = BINGO_GRID_SIZE * BINGO_GRID_SIZE
|
||||||
3
packages/shared/src/index.ts
Normal file
3
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./constants"
|
||||||
|
export * from "./room-types"
|
||||||
|
export * from "./ws-messages"
|
||||||
33
packages/shared/src/room-types.ts
Normal file
33
packages/shared/src/room-types.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { ACTS } from "./constants"
|
||||||
|
|
||||||
|
export const playerSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
sessionId: z.string().uuid(),
|
||||||
|
displayName: z.string().min(1).max(20),
|
||||||
|
isHost: z.boolean(),
|
||||||
|
connected: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Player = z.infer<typeof playerSchema>
|
||||||
|
|
||||||
|
export const roomStateSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
code: z.string().length(4),
|
||||||
|
currentAct: z.enum(ACTS),
|
||||||
|
hostSessionId: z.string().uuid(),
|
||||||
|
players: z.array(playerSchema),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
expiresAt: z.string().datetime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RoomState = z.infer<typeof roomStateSchema>
|
||||||
|
|
||||||
|
/** Client-side room state — includes the current player's session info */
|
||||||
|
export interface ClientRoomState {
|
||||||
|
room: RoomState | null
|
||||||
|
mySessionId: string | null
|
||||||
|
myPlayerId: string | null
|
||||||
|
isHost: boolean
|
||||||
|
connectionStatus: "disconnected" | "connecting" | "connected"
|
||||||
|
}
|
||||||
83
packages/shared/src/ws-messages.ts
Normal file
83
packages/shared/src/ws-messages.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { ACTS } from "./constants"
|
||||||
|
import { playerSchema, roomStateSchema } from "./room-types"
|
||||||
|
|
||||||
|
// ─── Client → Server ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const joinRoomMessage = z.object({
|
||||||
|
type: z.literal("join_room"),
|
||||||
|
displayName: z.string().min(1).max(20),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const reconnectMessage = z.object({
|
||||||
|
type: z.literal("reconnect"),
|
||||||
|
sessionId: z.string().uuid(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const advanceActMessage = z.object({
|
||||||
|
type: z.literal("advance_act"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const endRoomMessage = z.object({
|
||||||
|
type: z.literal("end_room"),
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Union of all client → server messages (room system only — games add more) */
|
||||||
|
export const clientMessage = z.discriminatedUnion("type", [
|
||||||
|
joinRoomMessage,
|
||||||
|
reconnectMessage,
|
||||||
|
advanceActMessage,
|
||||||
|
endRoomMessage,
|
||||||
|
])
|
||||||
|
|
||||||
|
export type ClientMessage = z.infer<typeof clientMessage>
|
||||||
|
|
||||||
|
// ─── Server → Client ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const roomStateMessage = z.object({
|
||||||
|
type: z.literal("room_state"),
|
||||||
|
room: roomStateSchema,
|
||||||
|
sessionId: z.string().uuid().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const playerJoinedMessage = z.object({
|
||||||
|
type: z.literal("player_joined"),
|
||||||
|
player: playerSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const playerDisconnectedMessage = z.object({
|
||||||
|
type: z.literal("player_disconnected"),
|
||||||
|
playerId: z.string().uuid(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const playerReconnectedMessage = z.object({
|
||||||
|
type: z.literal("player_reconnected"),
|
||||||
|
playerId: z.string().uuid(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const actChangedMessage = z.object({
|
||||||
|
type: z.literal("act_changed"),
|
||||||
|
newAct: z.enum(ACTS),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const roomEndedMessage = z.object({
|
||||||
|
type: z.literal("room_ended"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const errorMessage = z.object({
|
||||||
|
type: z.literal("error"),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Union of all server → client messages (room system only) */
|
||||||
|
export const serverMessage = z.discriminatedUnion("type", [
|
||||||
|
roomStateMessage,
|
||||||
|
playerJoinedMessage,
|
||||||
|
playerDisconnectedMessage,
|
||||||
|
playerReconnectedMessage,
|
||||||
|
actChangedMessage,
|
||||||
|
roomEndedMessage,
|
||||||
|
errorMessage,
|
||||||
|
])
|
||||||
|
|
||||||
|
export type ServerMessage = z.infer<typeof serverMessage>
|
||||||
8
packages/shared/tsconfig.json
Normal file
8
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user