diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bc93bce --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL=postgresql://localhost:5433/celebrate_esc +PORT=3001 diff --git a/.gitignore b/.gitignore index e458ed5..f886bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,23 @@ +# worktrees .worktrees/ + +# mise +.mise.local.toml + +# dependencies +node_modules/ + +# environment +.env +.env.local +.env.*.local + +# build output +dist/ + +# OS +.DS_Store + +# IDE +.vscode/ +.idea/ diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..6ecbdaa --- /dev/null +++ b/.mise.toml @@ -0,0 +1,3 @@ +[tools] +bun = "1.3.10" +node = "22.14.0" diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..1813aef --- /dev/null +++ b/biome.json @@ -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"] + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..377ea75 --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..802b6c8 --- /dev/null +++ b/package.json @@ -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 ." + } +} diff --git a/packages/client/components.json b/packages/client/components.json new file mode 100644 index 0000000..b10db0c --- /dev/null +++ b/packages/client/components.json @@ -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" +} diff --git a/packages/client/index.html b/packages/client/index.html new file mode 100644 index 0000000..90d8d65 --- /dev/null +++ b/packages/client/index.html @@ -0,0 +1,12 @@ + + + + + + ESC Party + + +
+ + + diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 0000000..dc4e798 --- /dev/null +++ b/packages/client/package.json @@ -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" + } +} diff --git a/packages/client/src/app.css b/packages/client/src/app.css new file mode 100644 index 0000000..75521db --- /dev/null +++ b/packages/client/src/app.css @@ -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); + } +} diff --git a/packages/client/src/components/player-list.tsx b/packages/client/src/components/player-list.tsx new file mode 100644 index 0000000..09e7364 --- /dev/null +++ b/packages/client/src/components/player-list.tsx @@ -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 ( +
+

Players ({players.length})

+ +
+ ) +} diff --git a/packages/client/src/components/room-header.tsx b/packages/client/src/components/room-header.tsx new file mode 100644 index 0000000..6674965 --- /dev/null +++ b/packages/client/src/components/room-header.tsx @@ -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 = { + lobby: "Lobby", + act1: "Act 1", + act2: "Act 2", + act3: "Act 3", + ended: "Ended", +} + +export function RoomHeader({ roomCode, currentAct, connectionStatus }: RoomHeaderProps) { + return ( +
+
+ {roomCode} + {actLabels[currentAct]} +
+ +
+ ) +} diff --git a/packages/client/src/components/ui/badge.tsx b/packages/client/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/packages/client/src/components/ui/badge.tsx @@ -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, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/packages/client/src/components/ui/button.tsx b/packages/client/src/components/ui/button.tsx new file mode 100644 index 0000000..36496a2 --- /dev/null +++ b/packages/client/src/components/ui/button.tsx @@ -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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/packages/client/src/components/ui/card.tsx b/packages/client/src/components/ui/card.tsx new file mode 100644 index 0000000..f62edea --- /dev/null +++ b/packages/client/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/packages/client/src/components/ui/input.tsx b/packages/client/src/components/ui/input.tsx new file mode 100644 index 0000000..68551b9 --- /dev/null +++ b/packages/client/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/packages/client/src/components/ui/tabs.tsx b/packages/client/src/components/ui/tabs.tsx new file mode 100644 index 0000000..f57fffd --- /dev/null +++ b/packages/client/src/components/ui/tabs.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/packages/client/src/hooks/use-websocket.ts b/packages/client/src/hooks/use-websocket.ts new file mode 100644 index 0000000..e96a7a6 --- /dev/null +++ b/packages/client/src/hooks/use-websocket.ts @@ -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(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 } +} diff --git a/packages/client/src/lib/utils.ts b/packages/client/src/lib/utils.ts new file mode 100644 index 0000000..9a7122c --- /dev/null +++ b/packages/client/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/packages/client/src/main.tsx b/packages/client/src/main.tsx new file mode 100644 index 0000000..27872b4 --- /dev/null +++ b/packages/client/src/main.tsx @@ -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( + + + , +) diff --git a/packages/client/src/routeTree.gen.ts b/packages/client/src/routeTree.gen.ts new file mode 100644 index 0000000..4ae3d55 --- /dev/null +++ b/packages/client/src/routeTree.gen.ts @@ -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() diff --git a/packages/client/src/routes/__root.tsx b/packages/client/src/routes/__root.tsx new file mode 100644 index 0000000..58f14d3 --- /dev/null +++ b/packages/client/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { createRootRoute, Outlet } from "@tanstack/react-router" + +export const Route = createRootRoute({ + component: () => ( +
+ +
+ ), +}) diff --git a/packages/client/src/routes/display.$roomCode.tsx b/packages/client/src/routes/display.$roomCode.tsx new file mode 100644 index 0000000..f43fa7a --- /dev/null +++ b/packages/client/src/routes/display.$roomCode.tsx @@ -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 ( +
+

+ {connectionStatus === "connecting" ? "Connecting..." : "Room not found"} +

+
+ ) + } + + return ( +
+ +
+ {room.currentAct === "lobby" && } + +
+
+ ) +} + +function LobbyDisplay({ roomCode }: { roomCode: string }) { + const joinUrl = `${window.location.origin}/play/${roomCode}` + + return ( +
+

Join the party!

+
+ {roomCode} +
+

+ Go to {joinUrl} +

+

or scan the QR code

+ {/* QR code will be added in Plan 5 (polish) */} +
+ QR code +
+
+ ) +} diff --git a/packages/client/src/routes/host.$roomCode.tsx b/packages/client/src/routes/host.$roomCode.tsx new file mode 100644 index 0000000..461e5ef --- /dev/null +++ b/packages/client/src/routes/host.$roomCode.tsx @@ -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> = { + 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 ( +
+

+ {connectionStatus === "connecting" ? "Connecting..." : "Room not found"} +

+
+ ) + } + + return ( +
+ + + + + Play + + + Host + + + + + {/* Game UI will be added in later plans */} + + +
+ + + Room Controls + + + {room.currentAct !== "ended" && ( + + )} + {room.currentAct !== "ended" && ( + + )} + {room.currentAct === "ended" && ( +

+ The party has ended. Thanks for playing! +

+ )} +
+
+ +
+
+
+
+ ) +} diff --git a/packages/client/src/routes/index.tsx b/packages/client/src/routes/index.tsx new file mode 100644 index 0000000..28c1c94 --- /dev/null +++ b/packages/client/src/routes/index.tsx @@ -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 ( +
+

ESC Party

+

Eurovision Song Contest — Party Companion

+
+ + +
+
+ ) +} + +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 ( + + + Host a Party + + + setDisplayName(e.target.value)} + maxLength={20} + onKeyDown={(e) => e.key === "Enter" && handleCreate()} + /> + + {error &&

{error}

} +
+
+ ) +} + +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 ( + + + Join a Party + + + setRoomCode(e.target.value.toUpperCase())} + maxLength={4} + className="text-center text-2xl tracking-widest" + /> + setDisplayName(e.target.value)} + maxLength={20} + onKeyDown={(e) => e.key === "Enter" && handleJoin()} + /> + + + + ) +} diff --git a/packages/client/src/routes/play.$roomCode.tsx b/packages/client/src/routes/play.$roomCode.tsx new file mode 100644 index 0000000..2ed1744 --- /dev/null +++ b/packages/client/src/routes/play.$roomCode.tsx @@ -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 ( +
+

+ {connectionStatus === "connecting" ? "Connecting..." : "Room not found"} +

+
+ ) + } + + // 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 ( +
+

Join Room {roomCode}

+ setManualName(e.target.value)} + maxLength={20} + onKeyDown={(e) => { + if (e.key === "Enter" && manualName.trim()) { + joinSentRef.current = true + send({ type: "join_room", displayName: manualName.trim() }) + } + }} + /> + +
+ ) + } + + if (!mySessionId) { + return ( +
+

Joining room...

+
+ ) + } + + return ( +
+ +
+ {room.currentAct === "lobby" && ( +
+

Waiting for the host to start...

+
+ )} + {room.currentAct === "ended" && ( +
+

The party has ended. Thanks for playing!

+
+ )} + {/* Game UI will be added in later plans */} + +
+
+ ) +} diff --git a/packages/client/src/stores/room-store.ts b/packages/client/src/stores/room-store.ts new file mode 100644 index 0000000..fe782c5 --- /dev/null +++ b/packages/client/src/stores/room-store.ts @@ -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((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" }), +})) diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json new file mode 100644 index 0000000..e60b8ea --- /dev/null +++ b/packages/client/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "outDir": "dist", + "rootDir": "src", + "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts new file mode 100644 index 0000000..c6c7ef0 --- /dev/null +++ b/packages/client/vite.config.ts @@ -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, + }, + }, + }, +}) diff --git a/packages/server/data/scoring.json b/packages/server/data/scoring.json new file mode 100644 index 0000000..f3452ce --- /dev/null +++ b/packages/server/data/scoring.json @@ -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 +} diff --git a/packages/server/drizzle.config.ts b/packages/server/drizzle.config.ts new file mode 100644 index 0000000..a5e5683 --- /dev/null +++ b/packages/server/drizzle.config.ts @@ -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!, + }, +}) diff --git a/packages/server/drizzle/0000_new_phantom_reporter.sql b/packages/server/drizzle/0000_new_phantom_reporter.sql new file mode 100644 index 0000000..94b586c --- /dev/null +++ b/packages/server/drizzle/0000_new_phantom_reporter.sql @@ -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; \ No newline at end of file diff --git a/packages/server/drizzle/meta/0000_snapshot.json b/packages/server/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..75d71fe --- /dev/null +++ b/packages/server/drizzle/meta/0000_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/packages/server/drizzle/meta/_journal.json b/packages/server/drizzle/meta/_journal.json new file mode 100644 index 0000000..700f923 --- /dev/null +++ b/packages/server/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1773228528975, + "tag": "0000_new_phantom_reporter", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..f5f2a5a --- /dev/null +++ b/packages/server/package.json @@ -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" + } +} diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts new file mode 100644 index 0000000..5154acb --- /dev/null +++ b/packages/server/src/app.ts @@ -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 } diff --git a/packages/server/src/db/client.ts b/packages/server/src/db/client.ts new file mode 100644 index 0000000..96385d2 --- /dev/null +++ b/packages/server/src/db/client.ts @@ -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 diff --git a/packages/server/src/db/schema.ts b/packages/server/src/db/schema.ts new file mode 100644 index 0000000..d3525ce --- /dev/null +++ b/packages/server/src/db/schema.ts @@ -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(), + 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"), +}) diff --git a/packages/server/src/env.ts b/packages/server/src/env.ts new file mode 100644 index 0000000..931b8d6 --- /dev/null +++ b/packages/server/src/env.ts @@ -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) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..df95848 --- /dev/null +++ b/packages/server/src/index.ts @@ -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) diff --git a/packages/server/src/rooms/index.ts b/packages/server/src/rooms/index.ts new file mode 100644 index 0000000..e153bf2 --- /dev/null +++ b/packages/server/src/rooms/index.ts @@ -0,0 +1,3 @@ +import { RoomManager } from "./room-manager" + +export const roomManager = new RoomManager() diff --git a/packages/server/src/rooms/room-manager.ts b/packages/server/src/rooms/room-manager.ts new file mode 100644 index 0000000..1605ec2 --- /dev/null +++ b/packages/server/src/rooms/room-manager.ts @@ -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 // sessionId -> player + createdAt: Date + expiresAt: Date +} + +export class RoomManager { + private rooms = new Map() + + 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 + } +} diff --git a/packages/server/src/rooms/room-service.ts b/packages/server/src/rooms/room-service.ts new file mode 100644 index 0000000..48dacf6 --- /dev/null +++ b/packages/server/src/rooms/room-service.ts @@ -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)) + } +} diff --git a/packages/server/src/ws/handler.ts b/packages/server/src/ws/handler.ts new file mode 100644 index 0000000..82322e6 --- /dev/null +++ b/packages/server/src/ws/handler.ts @@ -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>() + +function getConnections(roomCode: string): Set { + 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, + }) + } + } + } + }, + } + }), + ) +} diff --git a/packages/server/tests/room-manager.test.ts b/packages/server/tests/room-manager.test.ts new file mode 100644 index 0000000..8bf97ee --- /dev/null +++ b/packages/server/tests/room-manager.test.ts @@ -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() + 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" }) + }) + }) +}) diff --git a/packages/server/tests/ws-handler.test.ts b/packages/server/tests/ws-handler.test.ts new file mode 100644 index 0000000..05b5183 --- /dev/null +++ b/packages/server/tests/ws-handler.test.ts @@ -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 + +function waitForMessage(ws: WebSocket): Promise { + return new Promise((resolve) => { + ws.addEventListener( + "message", + (event) => { + resolve(JSON.parse(event.data as string)) + }, + { once: true }, + ) + }) +} + +function waitForOpen(ws: WebSocket): Promise { + 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() + }) +}) diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..cd108a3 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..01191b7 --- /dev/null +++ b/packages/shared/package.json @@ -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" + } +} diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts new file mode 100644 index 0000000..a4b324f --- /dev/null +++ b/packages/shared/src/constants.ts @@ -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 diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..c5383e2 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,3 @@ +export * from "./constants" +export * from "./room-types" +export * from "./ws-messages" diff --git a/packages/shared/src/room-types.ts b/packages/shared/src/room-types.ts new file mode 100644 index 0000000..a7d7a2b --- /dev/null +++ b/packages/shared/src/room-types.ts @@ -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 + +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 + +/** 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" +} diff --git a/packages/shared/src/ws-messages.ts b/packages/shared/src/ws-messages.ts new file mode 100644 index 0000000..722501e --- /dev/null +++ b/packages/shared/src/ws-messages.ts @@ -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 + +// ─── 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 diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..cd108a3 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6f51a8a --- /dev/null +++ b/tsconfig.json @@ -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 + } +}