feat!: upgrade to SvelteKit 3 and adapter-bun
Some checks failed
Test and deploy staging / staging (push) Failing after 14s
Some checks failed
Test and deploy staging / staging (push) Failing after 14s
- run sv migrate sveltekit-3: config moves into vite.config.ts, $lib becomes #lib, $env/dynamic becomes $app/env/private via src/env.ts, $app/environment becomes $app/env, hook types move to @sveltejs/kit/hooks - env vars stay optional (the migrator defaulted them to ''), and BANCHO_MODE, which it missed, is declared and passed to simulatorEnabled - handleError only logs and masks unknown errors; Kit 3 now routes 404s and error(...) through it too - json()/text() become Response.json()/new Response(); goto keepFocus/noScroll becomes reset: false; invalidateAll becomes refreshAll; page.url is copied before mutating - replace adapter-node with @sveltejs/adapter-bun; prod-server.ts wraps Bun.serve to add native WebSockets (bunSockets) for /ws paths, since the generated server has no websocket option - drop the Kit readdir patch: Kit 3 sorts route entries itself - match view and broadcast pages use the live score modules BREAKING CHANGE: requires SvelteKit 3, TypeScript 6 and Bun 1.4; the build output is build/index.js from adapter-bun, started through prod-server.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
FROM oven/bun:1.4.0 AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock ./
|
||||
# Patched dependencies are applied during install.
|
||||
COPY patches ./patches
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
ENV NODE_ENV=production
|
||||
|
||||
112
bun.lock
112
bun.lock
@@ -17,8 +17,9 @@
|
||||
"@lucide/svelte": "^1.48.0",
|
||||
"@playwright/test": "^1.63.0",
|
||||
"@sentry/sveltekit": "^10.75.3",
|
||||
"@sveltejs/adapter-node": "^5.5.7",
|
||||
"@sveltejs/kit": "^2.70.3",
|
||||
"@sveltejs/adapter-bun": "^1.0.0",
|
||||
"@sveltejs/kit": "^3.0.0",
|
||||
"@sveltejs/load-config": "^0.2.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
@@ -51,7 +52,7 @@
|
||||
"tailwind-variants": "^3.3.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript": "^6.0.0",
|
||||
"typescript-eslint": "^8.70.1",
|
||||
"vite": "^8.3.1",
|
||||
"vite-plugin-devtools-json": "^1.1.0",
|
||||
@@ -59,9 +60,6 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@sveltejs/kit@2.70.3": "patches/@sveltejs%2Fkit@2.70.3.patch",
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
@@ -281,8 +279,6 @@
|
||||
|
||||
"@lucide/svelte": ["@lucide/svelte@1.48.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-bLimVDjehxu3fpu9XEUpqEkBk2sJAHAT2sVcLHhlM70EdNsXuj9ZnYENWv5xgw3+05h4i5TirAxTiTgnZZf8gw=="],
|
||||
|
||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@2.4.0", "", {}, "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="],
|
||||
@@ -343,66 +339,6 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg=="],
|
||||
|
||||
"@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
|
||||
|
||||
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
|
||||
|
||||
"@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.5", "", { "os": "android", "cpu": "arm" }, "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.5", "", { "os": "android", "cpu": "arm64" }, "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.5", "", { "os": "none", "cpu": "arm64" }, "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw=="],
|
||||
|
||||
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
|
||||
|
||||
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
|
||||
@@ -467,9 +403,9 @@
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
|
||||
|
||||
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.7", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-replace": "^6.0.3", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g=="],
|
||||
"@sveltejs/adapter-bun": ["@sveltejs/adapter-bun@1.0.0", "", { "dependencies": { "bun-types": "^1.3.14" }, "peerDependencies": { "@sveltejs/kit": "^3.0.0-next.0" } }, "sha512-CUfFNxrWrKk/0VqSM1DrXCAwpY3tWocaFwBohuBeyxKx07859shLqPdT9oo00lDti9PvVwhQMDS6bKQylTyv0g=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="],
|
||||
"@sveltejs/kit": ["@sveltejs/kit@3.1.0", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1", "@standard-schema/spec": "^1.1.0", "cookie": "^2.0.1", "devalue": "^5.9.4", "esm-env": "^1.2.2", "magic-string": "^1.2.3", "mrmime": "^2.0.1", "sirv": "^3.0.2" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.57.1", "typescript": "^6.0.0", "vite": "^8.0.12" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JTWlq2HQlkqx6vIoxhC0R6sc4WVqbQoRyEmr5YlqKmiYbzY1U3awtSWTmjMLlS056a49AmkQR6rnNANXIE8B0Q=="],
|
||||
|
||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
||||
|
||||
@@ -515,8 +451,6 @@
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
@@ -527,8 +461,6 @@
|
||||
|
||||
"@types/node": ["@types/node@25.9.8", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-VfMrScDmMhUJQmd5hArdQnFvK0OIeD36uN2Va1FcpYaLB/BgkgM8Ulc50XtYISPMz7APJ30+N0T5EM0jlcdfRw=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.70.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.70.1", "@typescript-eslint/type-utils": "8.70.1", "@typescript-eslint/utils": "8.70.1", "@typescript-eslint/visitor-keys": "8.70.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.70.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA=="],
|
||||
@@ -643,8 +575,6 @@
|
||||
|
||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
|
||||
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||
|
||||
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
|
||||
|
||||
"conventional-changelog-angular": ["conventional-changelog-angular@8.3.1", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg=="],
|
||||
@@ -655,7 +585,7 @@
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
|
||||
|
||||
@@ -707,8 +637,6 @@
|
||||
|
||||
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.52.0", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="],
|
||||
@@ -741,7 +669,7 @@
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
@@ -773,8 +701,6 @@
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
@@ -801,8 +727,6 @@
|
||||
|
||||
"hashery": ["hashery@1.5.1", "", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
||||
|
||||
"hls.js": ["hls.js@1.7.3", "", {}, "sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A=="],
|
||||
@@ -829,16 +753,12 @@
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||
|
||||
"is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
@@ -869,8 +789,6 @@
|
||||
|
||||
"keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
|
||||
|
||||
"kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="],
|
||||
@@ -979,8 +897,6 @@
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@@ -1043,8 +959,6 @@
|
||||
|
||||
"require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
|
||||
|
||||
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||
|
||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
@@ -1055,8 +969,6 @@
|
||||
|
||||
"rolldown": ["rolldown@1.2.11", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.11", "@rolldown/binding-android-arm64": "1.2.11", "@rolldown/binding-darwin-arm64": "1.2.11", "@rolldown/binding-darwin-x64": "1.2.11", "@rolldown/binding-freebsd-x64": "1.2.11", "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", "@rolldown/binding-linux-arm64-gnu": "1.2.11", "@rolldown/binding-linux-arm64-musl": "1.2.11", "@rolldown/binding-linux-ppc64-gnu": "1.2.11", "@rolldown/binding-linux-s390x-gnu": "1.2.11", "@rolldown/binding-linux-x64-gnu": "1.2.11", "@rolldown/binding-linux-x64-musl": "1.2.11", "@rolldown/binding-openharmony-arm64": "1.2.11", "@rolldown/binding-win32-arm64-msvc": "1.2.11", "@rolldown/binding-win32-x64-msvc": "1.2.11" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw=="],
|
||||
|
||||
"rollup": ["rollup@4.63.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.5", "@rollup/rollup-android-arm64": "4.63.5", "@rollup/rollup-darwin-arm64": "4.63.5", "@rollup/rollup-darwin-x64": "4.63.5", "@rollup/rollup-freebsd-arm64": "4.63.5", "@rollup/rollup-freebsd-x64": "4.63.5", "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", "@rollup/rollup-linux-arm-musleabihf": "4.63.5", "@rollup/rollup-linux-arm64-gnu": "4.63.5", "@rollup/rollup-linux-arm64-musl": "4.63.5", "@rollup/rollup-linux-loong64-gnu": "4.63.5", "@rollup/rollup-linux-loong64-musl": "4.63.5", "@rollup/rollup-linux-ppc64-gnu": "4.63.5", "@rollup/rollup-linux-ppc64-musl": "4.63.5", "@rollup/rollup-linux-riscv64-gnu": "4.63.5", "@rollup/rollup-linux-riscv64-musl": "4.63.5", "@rollup/rollup-linux-s390x-gnu": "4.63.5", "@rollup/rollup-linux-x64-gnu": "4.63.5", "@rollup/rollup-linux-x64-musl": "4.63.5", "@rollup/rollup-openbsd-x64": "4.63.5", "@rollup/rollup-openharmony-arm64": "4.63.5", "@rollup/rollup-win32-arm64-msvc": "4.63.5", "@rollup/rollup-win32-ia32-msvc": "4.63.5", "@rollup/rollup-win32-x64-gnu": "4.63.5", "@rollup/rollup-win32-x64-msvc": "4.63.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw=="],
|
||||
|
||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
|
||||
@@ -1111,8 +1023,6 @@
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"svelte": ["svelte@5.57.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.6.0", "@sveltejs/acorn-typescript": "^1.0.13", "@types/estree": "^1.0.9", "acorn": "^8.18.0", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.9.2", "esm-env": "^1.2.1", "esrap": "^2.3.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
|
||||
@@ -1161,7 +1071,7 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.70.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.70.1", "@typescript-eslint/parser": "8.70.1", "@typescript-eslint/typescript-estree": "8.70.1", "@typescript-eslint/utils": "8.70.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA=="],
|
||||
|
||||
@@ -1231,7 +1141,7 @@
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||
"@sveltejs/kit/magic-string": ["magic-string@1.4.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@1.4.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="],
|
||||
|
||||
@@ -1253,8 +1163,6 @@
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.10", "", {}, "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q=="],
|
||||
|
||||
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.3.2", "", {}, "sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="],
|
||||
|
||||
@@ -6,7 +6,9 @@ import svelte from 'eslint-plugin-svelte';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
import { loadConfig } from '@sveltejs/load-config';
|
||||
|
||||
const svelteConfig = (await loadConfig('./', { traverse: false }))?.config;
|
||||
|
||||
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
|
||||
|
||||
|
||||
13
package.json
13
package.json
@@ -18,6 +18,7 @@
|
||||
"test:e2e": "playwright test",
|
||||
"test:streamer": "bun test streamer/",
|
||||
"streamer": "bun streamer/agent.ts",
|
||||
"broadcast:build": "bun build streamer/windows/cli.ts --compile --minify --target=bun-windows-x64 --outfile dist/vash-broadcast.exe",
|
||||
"streamer:build": "bun build streamer/agent.ts --compile --minify --target=bun-windows-x64 --outfile dist/vash-streamer.exe && bun build streamer/agent.ts --compile --minify --target=bun-linux-x64 --outfile dist/vash-streamer",
|
||||
"db:seed": "bun scripts/seed-dev.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
@@ -34,8 +35,9 @@
|
||||
"@lucide/svelte": "^1.48.0",
|
||||
"@playwright/test": "^1.63.0",
|
||||
"@sentry/sveltekit": "^10.75.3",
|
||||
"@sveltejs/adapter-node": "^5.5.7",
|
||||
"@sveltejs/kit": "^2.70.3",
|
||||
"@sveltejs/adapter-bun": "^1.0.0",
|
||||
"@sveltejs/kit": "^3.0.0",
|
||||
"@sveltejs/load-config": "^0.2.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
@@ -68,7 +70,7 @@
|
||||
"tailwind-variants": "^3.3.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript": "^6.0.0",
|
||||
"typescript-eslint": "^8.70.1",
|
||||
"vite": "^8.3.1",
|
||||
"vite-plugin-devtools-json": "^1.1.0",
|
||||
@@ -88,7 +90,8 @@
|
||||
"discord.js": "^14.27.0",
|
||||
"zod": "^4.6.5"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@sveltejs/kit@2.70.3": "patches/@sveltejs%2Fkit@2.70.3.patch"
|
||||
"imports": {
|
||||
"#lib": "./src/lib/index.js",
|
||||
"#lib/*": "./src/lib/*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
diff --git a/src/core/sync/create_manifest_data/index.js b/src/core/sync/create_manifest_data/index.js
|
||||
index 6e97903682f448a9c95a01527530bd0bdb545174..d233d9c10da549f31bb4d4ff48204bb9f9b4a9e9 100644
|
||||
--- a/src/core/sync/create_manifest_data/index.js
|
||||
+++ b/src/core/sync/create_manifest_data/index.js
|
||||
@@ -222,7 +222,9 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
||||
|
||||
// We can't use withFileTypes because of a NodeJs bug which returns wrong results
|
||||
// with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
|
||||
- const files = fs.readdirSync(dir).map((name) => ({
|
||||
+ // Sorted so route nodes get the same numbers under every runtime: Node
|
||||
+ // returns directory entries sorted, Bun returns them in on-disk order.
|
||||
+ const files = fs.readdirSync(dir).sort().map((name) => ({
|
||||
is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
|
||||
name
|
||||
}));
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { handler } from './build/handler.js';
|
||||
import { attachSockets } from './src/lib/server/ws-attach.ts';
|
||||
/**
|
||||
* Production entry: the server adapter-bun generates, plus the app's WebSockets.
|
||||
* The generated server calls Bun.serve itself and has no websocket option, so
|
||||
* Bun.serve is wrapped before it loads to upgrade /ws paths first. Everything
|
||||
* else (static assets, HOST/PORT/*_HEADER env, graceful shutdown) is the adapter's.
|
||||
*/
|
||||
import { bunSockets } from './src/lib/server/ws-attach.ts';
|
||||
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
const server = createServer(handler);
|
||||
await attachSockets(server);
|
||||
server.listen(port, host, () => {
|
||||
console.log(`Vash Esports listening on ${host}:${port}`);
|
||||
});
|
||||
const serve = Bun.serve;
|
||||
Bun.serve = ((options: Parameters<typeof serve>[0] & { fetch: Parameters<typeof bunSockets>[0] }) =>
|
||||
serve({ ...options, ...bunSockets(options.fetch) })) as typeof serve;
|
||||
|
||||
await import('./build/index.js');
|
||||
|
||||
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
// src/app.d.ts
|
||||
import type { Session } from 'better-auth/minimal';
|
||||
import type { user } from '$lib/server/db/auth.schema';
|
||||
import type { ConnectionProfile } from '$lib/auth-identity';
|
||||
import type { user } from '#lib/server/db/auth.schema.js';
|
||||
import type { ConnectionProfile } from '#lib/auth-identity.js';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
|
||||
41
src/env.ts
Normal file
41
src/env.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { defineEnvVars } from '@sveltejs/kit/env';
|
||||
|
||||
/** Every variable is optional and read at startup, like the old `$env/dynamic` modules. */
|
||||
const optional = (input: string | undefined) => input;
|
||||
|
||||
export const variables = defineEnvVars({
|
||||
PUBLIC_UMAMI_URL: { public: true, schema: optional },
|
||||
PUBLIC_UMAMI_WEBSITE_ID: { public: true, schema: optional },
|
||||
ORIGIN: { schema: optional },
|
||||
APP_ENV: { schema: optional },
|
||||
RESEND_API_KEY: { schema: optional },
|
||||
EMAIL_FROM: { schema: optional },
|
||||
OSU_CLIENT_ID: { schema: optional },
|
||||
OSU_CLIENT_SECRET: { schema: optional },
|
||||
DISCORD_CLIENT_ID: { schema: optional },
|
||||
DISCORD_CLIENT_SECRET: { schema: optional },
|
||||
GOOGLE_CLIENT_ID: { schema: optional },
|
||||
GOOGLE_CLIENT_SECRET: { schema: optional },
|
||||
BETTER_AUTH_SECRET: { schema: optional },
|
||||
R2_PUBLIC_URL: { schema: optional },
|
||||
STREAM_KEY_SECRET: { schema: optional },
|
||||
R2_ACCESS_KEY_ID: { schema: optional },
|
||||
R2_SECRET_ACCESS_KEY: { schema: optional },
|
||||
R2_ACCOUNT_ID: { schema: optional },
|
||||
R2_BUCKET: { schema: optional },
|
||||
ROOT_ADMIN_USER_ID: { schema: optional },
|
||||
BANCHO_MODE: { schema: optional },
|
||||
OSU_IRC_ACCOUNTS: { schema: optional },
|
||||
OSU_IRC_USERNAME: { schema: optional },
|
||||
OSU_IRC_PASSWORD: { schema: optional },
|
||||
OSU_IRC_HOST: { schema: optional },
|
||||
OSU_IRC_PORT: { schema: optional },
|
||||
DISCORD_BOT_CLIENT_ID: { schema: optional },
|
||||
DISCORD_BOT_TOKEN: { schema: optional },
|
||||
DISCORD_GUILD_ID: { schema: optional },
|
||||
DISCORD_MATCH_CHANNEL_ID: { schema: optional },
|
||||
DISCORD_STREAM_CHANNEL_ID: { schema: optional },
|
||||
DATABASE_URL: { schema: optional },
|
||||
DB_URL: { schema: optional },
|
||||
LOG_SQL: { schema: optional }
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { HandleClientError } from '@sveltejs/kit/hooks';
|
||||
|
||||
// src/hooks.client.ts
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.PUBLIC_SENTRY_DSN,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// src/hooks.server.ts
|
||||
import type { Handle, HandleServerError } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { building, dev } from '$app/environment';
|
||||
import { sequence, type Handle, type HandleServerError } from '@sveltejs/kit/hooks';
|
||||
|
||||
import { building, dev } from '$app/env';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { appEnv, titlePrefix } from '$lib/server/app-env';
|
||||
import { appEnv, titlePrefix } from '#lib/server/app-env.js';
|
||||
|
||||
if (!building) {
|
||||
Sentry.init({
|
||||
@@ -16,16 +16,16 @@ if (!building) {
|
||||
});
|
||||
}
|
||||
|
||||
import { auth } from '$lib/server/auth';
|
||||
import { auth } from '#lib/server/auth.js';
|
||||
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user as userTable } from '$lib/server/db/auth.schema';
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import { user as userTable } from '#lib/server/db/auth.schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ensureRootAdminRole } from '$lib/server/permissions';
|
||||
import { checkRateLimit } from '$lib/server/rate-limit';
|
||||
import { log } from '$lib/server/logger';
|
||||
import { metrics } from '$lib/server/metrics';
|
||||
import { startServices } from '$lib/server/services';
|
||||
import { ensureRootAdminRole } from '#lib/server/permissions/index.js';
|
||||
import { checkRateLimit } from '#lib/server/rate-limit.js';
|
||||
import { log } from '#lib/server/logger.js';
|
||||
import { metrics } from '#lib/server/metrics.js';
|
||||
import { startServices } from '#lib/server/services.js';
|
||||
|
||||
if (!building) {
|
||||
startServices().catch((err) => log.http.fatal({ err }, 'background services failed to start'));
|
||||
@@ -135,7 +135,10 @@ const handleAuth: Handle = async ({ event, resolve }) => {
|
||||
|
||||
export const handle: Handle = sequence(Sentry.sentryHandle(), handleRequestLogging, handleAuth);
|
||||
|
||||
export const handleError: HandleServerError = Sentry.handleErrorWithSentry(({ error, event }) => {
|
||||
export const handleError: HandleServerError = Sentry.handleErrorWithSentry((input) => {
|
||||
// Expected errors (error(...), 404s, validation) keep their own status and message.
|
||||
if (input.kind !== 'unknown') return;
|
||||
const { error, event } = input;
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
log.http.error(
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* What an overlay receives for its channel: the layout, the scene to show,
|
||||
* and everything its components draw from. One payload, pushed on every change.
|
||||
*/
|
||||
import type { LiveMatch } from '$lib/live';
|
||||
import type { MatchView } from '$lib/match-view';
|
||||
import type { Crowd } from '$lib/predictions';
|
||||
import type { LiveMatch } from '#lib/live.js';
|
||||
import type { MatchView } from '#lib/match-view.js';
|
||||
import type { Crowd } from '#lib/predictions.js';
|
||||
import type { Layout, SceneId } from './layout';
|
||||
|
||||
export type ChannelMode = 'auto' | 'match' | 'tournament';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* A made-up match in progress, so the layout editor can show every component
|
||||
* even when nothing is live.
|
||||
*/
|
||||
import type { MatchView, PlayerView, SideView, SlotView } from '$lib/match-view';
|
||||
import type { MatchView, PlayerView, SideView, SlotView } from '#lib/match-view.js';
|
||||
import type { ChannelPayload } from './channel';
|
||||
import type { Layout, SceneId } from './layout';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import type { LiveMatch } from '$lib/live';
|
||||
import type { LiveMatch } from '#lib/live.js';
|
||||
import { interest, pickMatch, RESULT_LINGER_MS } from './director';
|
||||
|
||||
function live(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* for it to be seen, and otherwise goes where it is most exciting: tournament
|
||||
* matches first, then close series near match point, tiebreakers, and crowds.
|
||||
*/
|
||||
import { DECIDED, type LiveMatch } from '$lib/live';
|
||||
import { DECIDED, type LiveMatch } from '#lib/live.js';
|
||||
import type { ChannelMode } from './channel';
|
||||
|
||||
export const RESULT_LINGER_MS = 30_000;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* layout editor. Coordinates are pixels on a 1920×1080 stage; the overlay
|
||||
* scales the stage to whatever size the browser source is.
|
||||
*/
|
||||
import type { MatchView, Phase } from '$lib/match-view';
|
||||
import type { MatchView, Phase } from '#lib/match-view.js';
|
||||
|
||||
export const STAGE = { width: 1920, height: 1080 } as const;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { hoursToRanges, type TimeRange } from '$lib/availability';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import { Label } from '#lib/components/ui/label/index.js';
|
||||
import { hoursToRanges, type TimeRange } from '#lib/availability.js';
|
||||
import { Clock3, Plus, Trash2, LocateFixed } from '@lucide/svelte';
|
||||
const {
|
||||
timezone,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { gradientFor, initials } from '$lib/format';
|
||||
import { gradientFor, initials } from '#lib/format.js';
|
||||
|
||||
/** A picture, or the initials on a gradient that is always the same for the same name. */
|
||||
const {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { TableMeta } from '$lib/server/table';
|
||||
import type { TableMeta } from '#lib/server/table.js';
|
||||
|
||||
interface Column {
|
||||
key: string;
|
||||
@@ -55,7 +55,7 @@
|
||||
});
|
||||
|
||||
function updateParams(updates: Record<string, string>) {
|
||||
const url = new URL(page.url);
|
||||
const url = new URL(page.url.href);
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value) {
|
||||
url.searchParams.set(key, value);
|
||||
@@ -67,7 +67,7 @@
|
||||
if (!('page' in updates)) {
|
||||
url.searchParams.set('page', '1');
|
||||
}
|
||||
goto(url.toString(), { keepFocus: true, noScroll: true });
|
||||
goto(url.toString(), { reset: false });
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import Avatar from '#lib/components/Avatar.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import { LIVE_STATES, type MatchSummary } from '$lib/match-summary';
|
||||
import { timeAgo } from '$lib/format';
|
||||
import Avatar from '#lib/components/Avatar.svelte';
|
||||
import { LIVE_STATES, type MatchSummary } from '#lib/match-summary.js';
|
||||
import { timeAgo } from '#lib/format.js';
|
||||
import Ban from '@lucide/svelte/icons/ban';
|
||||
import Crown from '@lucide/svelte/icons/crown';
|
||||
import Disc3 from '@lucide/svelte/icons/disc-3';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { SIDE_COLORS, initials } from '$lib/components/match/theme';
|
||||
import { SIDE_COLORS, initials } from '#lib/components/match/theme.js';
|
||||
import {
|
||||
POINTS,
|
||||
crowdShare,
|
||||
@@ -9,7 +9,7 @@
|
||||
type Crowd,
|
||||
type PredictionResult,
|
||||
type PredictionState
|
||||
} from '$lib/predictions';
|
||||
} from '#lib/predictions.js';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import Lock from '@lucide/svelte/icons/lock';
|
||||
import Sparkles from '@lucide/svelte/icons/sparkles';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import Avatar from '#lib/components/Avatar.svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/state';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import Avatar from '#lib/components/Avatar.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
interface SearchResult {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { queue, joinQueue, leaveQueue } from '$lib/stores/queue.svelte';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { queue, joinQueue, leaveQueue } from '#lib/stores/queue.svelte.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
|
||||
const user = $derived(page.data?.user);
|
||||
const activeMatch = $derived(page.data?.activeMatch);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { SIDE_COLORS, modColor } from '$lib/components/match/theme';
|
||||
import { SIDE_COLORS, modColor } from '#lib/components/match/theme.js';
|
||||
import { numberFormat, type OverlayProps } from './context';
|
||||
import Panel from './Panel.svelte';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Tween } from 'svelte/motion';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { SIDE_COLORS } from '$lib/components/match/theme';
|
||||
import { SIDE_COLORS } from '#lib/components/match/theme.js';
|
||||
import { numberFormat, type OverlayProps } from './context';
|
||||
|
||||
const { data, props, h }: OverlayProps = $props();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { flip } from 'svelte/animate';
|
||||
import { SIDE_COLORS, modColor } from '$lib/components/match/theme';
|
||||
import { SIDE_COLORS, modColor } from '#lib/components/match/theme.js';
|
||||
import type { OverlayProps } from './context';
|
||||
|
||||
const { data, props, w, h }: OverlayProps = $props();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { modColor } from '$lib/components/match/theme';
|
||||
import { modColor } from '#lib/components/match/theme.js';
|
||||
import type { OverlayProps } from './context';
|
||||
|
||||
const { data, props, h, accent }: OverlayProps = $props();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { ChannelPayload } from '$lib/broadcast/channel';
|
||||
import { stageOf, type ComponentType, type SceneId } from '$lib/broadcast/layout';
|
||||
import type { ChannelPayload } from '#lib/broadcast/channel.js';
|
||||
import { stageOf, type ComponentType, type SceneId } from '#lib/broadcast/layout.js';
|
||||
import type { OverlayProps } from './context';
|
||||
import Scoreboard from './Scoreboard.svelte';
|
||||
import LiveScore from './LiveScore.svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { SIDE_COLORS, flagUrl, initials } from '$lib/components/match/theme';
|
||||
import { SIDE_COLORS, flagUrl, initials } from '#lib/components/match/theme.js';
|
||||
import type { OverlayProps } from './context';
|
||||
|
||||
const { data }: OverlayProps = $props();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { SIDE_COLORS } from '$lib/components/match/theme';
|
||||
import { crowdShare } from '$lib/predictions';
|
||||
import { SIDE_COLORS } from '#lib/components/match/theme.js';
|
||||
import { crowdShare } from '#lib/predictions.js';
|
||||
import type { OverlayProps } from './context';
|
||||
import Panel from './Panel.svelte';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Overlay from './Overlay.svelte';
|
||||
import { demoChannel, demoPayload } from '$lib/broadcast/demo';
|
||||
import type { Layout, SceneId } from '$lib/broadcast/layout';
|
||||
import { demoChannel, demoPayload } from '#lib/broadcast/demo.js';
|
||||
import type { Layout, SceneId } from '#lib/broadcast/layout.js';
|
||||
|
||||
/** A small live render of a layout with the demo match; the checkerboard is where OBS shows through. */
|
||||
const { layout, scene = 'gameplay' }: { layout: Layout; scene?: SceneId } = $props();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { PHASE_LABELS } from '$lib/match-view';
|
||||
import { SIDE_COLORS, initials } from '$lib/components/match/theme';
|
||||
import { PHASE_LABELS } from '#lib/match-view.js';
|
||||
import { SIDE_COLORS, initials } from '#lib/components/match/theme.js';
|
||||
import type { OverlayProps } from './context';
|
||||
|
||||
const { data, props, h, accent }: OverlayProps = $props();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { SIDE_COLORS } from '$lib/components/match/theme';
|
||||
import { DECIDED } from '$lib/live';
|
||||
import { SIDE_COLORS } from '#lib/components/match/theme.js';
|
||||
import { DECIDED } from '#lib/live.js';
|
||||
import type { OverlayProps } from './context';
|
||||
|
||||
const { data, props, accent }: OverlayProps = $props();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { SIDE_COLORS } from '$lib/components/match/theme';
|
||||
import { SIDE_COLORS } from '#lib/components/match/theme.js';
|
||||
import type { OverlayProps } from './context';
|
||||
|
||||
const { data, now, h, accent }: OverlayProps = $props();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { scale } from 'svelte/transition';
|
||||
import { elasticOut } from 'svelte/easing';
|
||||
import { SIDE_COLORS, initials } from '$lib/components/match/theme';
|
||||
import { SIDE_COLORS, initials } from '#lib/components/match/theme.js';
|
||||
import type { OverlayProps } from './context';
|
||||
|
||||
const { data, props, h, accent }: OverlayProps = $props();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChannelPayload } from '$lib/broadcast/channel';
|
||||
import type { PlacedComponent } from '$lib/broadcast/layout';
|
||||
import type { ChannelPayload } from '#lib/broadcast/channel.js';
|
||||
import type { PlacedComponent } from '#lib/broadcast/layout.js';
|
||||
|
||||
/** What every overlay component gets. */
|
||||
export interface OverlayProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { PHASE_LABELS } from '$lib/match-view';
|
||||
import { DECIDED, type LiveMatch } from '$lib/live';
|
||||
import { SIDE_COLORS, initials, modColor } from '$lib/components/match/theme';
|
||||
import { PHASE_LABELS } from '#lib/match-view.js';
|
||||
import { DECIDED, type LiveMatch } from '#lib/live.js';
|
||||
import { SIDE_COLORS, initials, modColor } from '#lib/components/match/theme.js';
|
||||
import Eye from '@lucide/svelte/icons/eye';
|
||||
import Crown from '@lucide/svelte/icons/crown';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import type { SlotView } from '$lib/match-view';
|
||||
import * as Dialog from '#lib/components/ui/dialog/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import type { SlotView } from '#lib/match-view.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import MapCard from './MapCard.svelte';
|
||||
import CountdownRing from './CountdownRing.svelte';
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content
|
||||
class="max-h-[92dvh] overflow-y-auto border-2 sm:max-w-3xl"
|
||||
class="max-h-[92dvh] overflow-y-auto border-2 sm:max-w-4xl"
|
||||
style="border-color:{colors.ring}"
|
||||
>
|
||||
<Dialog.Header>
|
||||
@@ -70,7 +70,7 @@
|
||||
<Dialog.Title class="text-2xl font-black tracking-tight" style="color:{colors.text}"
|
||||
>{title}</Dialog.Title
|
||||
>
|
||||
<Dialog.Description>
|
||||
<Dialog.Description class="text-base">
|
||||
{#if prompt.kind === 'choose'}
|
||||
{view.rules.bansPerTeam > 0
|
||||
? 'Whoever picks first bans second.'
|
||||
|
||||
@@ -60,9 +60,11 @@
|
||||
</svg>
|
||||
{#if label}
|
||||
<span
|
||||
class="absolute font-mono font-semibold tabular-nums {size < 48 && text.length > 2
|
||||
? 'text-[9px]'
|
||||
: 'text-xs'} {urgent ? 'text-red-300' : 'text-zinc-200'}">{text}</span
|
||||
class="absolute font-mono font-semibold tabular-nums {size >= 48
|
||||
? 'text-base'
|
||||
: size < 40 && text.length > 2
|
||||
? 'text-[10px]'
|
||||
: 'text-xs'} {urgent ? 'text-red-300' : 'text-zinc-200'}">{text}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { SideView, SlotView } from '$lib/match-view';
|
||||
import type { SideView, SlotView } from '#lib/match-view.js';
|
||||
import { SIDE_COLORS, formatLength, modColor } from './theme';
|
||||
import Lock from '@lucide/svelte/icons/lock';
|
||||
import Trophy from '@lucide/svelte/icons/trophy';
|
||||
@@ -35,7 +35,7 @@
|
||||
const banned = $derived(slot.status === 'banned');
|
||||
const played = $derived(slot.status === 'played');
|
||||
const locked = $derived(slot.status === 'locked');
|
||||
const heights = { sm: 'h-[68px]', md: 'h-[92px]', lg: 'h-[132px]' };
|
||||
const heights = { sm: 'h-[68px]', md: 'h-[112px]', lg: 'h-[112px]' };
|
||||
</script>
|
||||
|
||||
<button
|
||||
@@ -90,12 +90,12 @@
|
||||
<div class="relative flex h-full flex-col justify-between p-2.5 {banned ? 'opacity-60' : ''}">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span
|
||||
class="rounded-md px-1.5 py-0.5 font-mono text-[11px] font-black tracking-wide shadow"
|
||||
class="rounded-md px-1.5 py-0.5 font-mono text-sm font-black tracking-wide shadow"
|
||||
style="background:{color};color:{slot.category === 'TB' ? '#18181b' : '#0a0a0a'}"
|
||||
>{slot.label}</span
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-md bg-black/50 px-1.5 py-0.5 text-[11px] font-semibold text-zinc-100 backdrop-blur"
|
||||
class="flex items-center gap-1 rounded-md bg-black/50 px-1.5 py-0.5 text-sm font-semibold text-zinc-100 backdrop-blur"
|
||||
>
|
||||
{#if locked}<Lock class="size-3" />{/if}
|
||||
{#if slot.starRating}<Star
|
||||
@@ -105,19 +105,21 @@
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate text-sm leading-tight font-semibold text-white {size === 'sm'
|
||||
? 'text-xs'
|
||||
: ''}"
|
||||
class="truncate leading-tight font-semibold text-white {size === 'lg'
|
||||
? 'text-xl'
|
||||
: size === 'md'
|
||||
? 'text-base'
|
||||
: 'text-sm'}"
|
||||
>
|
||||
{slot.title ?? `Beatmap #${slot.beatmapId}`}
|
||||
</div>
|
||||
{#if size !== 'sm'}
|
||||
<div class="truncate text-[11px] text-zinc-300/80">
|
||||
<div class="truncate text-sm text-zinc-300">
|
||||
{slot.artist ?? ''}{#if slot.version}<span class="text-zinc-400">
|
||||
[{slot.version}]</span
|
||||
>{/if}
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2.5 text-[10px] text-zinc-400">
|
||||
<div class="mt-1 flex items-center gap-2.5 text-xs text-zinc-300">
|
||||
{#if slot.bpm}<span class="flex items-center gap-0.5"
|
||||
><Music class="size-2.5" />{Math.round(slot.bpm)}</span
|
||||
>{/if}
|
||||
@@ -152,7 +154,7 @@
|
||||
{/if}
|
||||
{#if played}
|
||||
<div
|
||||
class="absolute right-2 bottom-2 flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase"
|
||||
class="absolute right-2 bottom-2 flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-bold uppercase"
|
||||
style="background:{winner ? SIDE_COLORS[winner.color].solid : '#3f3f46'};color:white"
|
||||
>
|
||||
<Trophy class="size-3" />{winner ? winner.name.slice(0, 14) : 'Draw'}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
{#if view.maps.length}
|
||||
<section class={embedded ? 'p-2' : 'rounded-2xl border border-white/8 bg-zinc-950/60 p-3 sm:p-4'}>
|
||||
<h2 class="mb-3 text-sm font-semibold tracking-wide text-zinc-200 uppercase">Maps played</h2>
|
||||
<h2 class="mb-3 text-base font-semibold tracking-wide text-zinc-200 uppercase">Maps played</h2>
|
||||
<ol class="space-y-2">
|
||||
{#each [...view.maps].reverse() as map (map.index)}
|
||||
{@const slot = live.slot(map.slotId)}
|
||||
@@ -43,29 +43,29 @@
|
||||
onclick={() => (open.has(map.index) ? open.delete(map.index) : open.add(map.index))}
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="w-4 shrink-0 text-center text-[11px] text-zinc-500 tabular-nums"
|
||||
<span class="w-4 shrink-0 text-center text-sm text-zinc-400 tabular-nums"
|
||||
>{map.index + 1}</span
|
||||
>
|
||||
<span
|
||||
class="shrink-0 font-mono text-xs font-black"
|
||||
class="shrink-0 font-mono text-sm font-black"
|
||||
style="color:{slot ? modColor(slot.category) : '#fff'}">{slot?.label ?? 'Map'}</span
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium"
|
||||
<span class="min-w-0 flex-1 truncate text-base font-medium"
|
||||
>{slot?.title ?? `Beatmap #${slot?.beatmapId ?? ''}`}</span
|
||||
>
|
||||
<ChevronDown
|
||||
class="size-4 shrink-0 text-zinc-500 transition {open.has(map.index)
|
||||
class="size-4 shrink-0 text-zinc-400 transition {open.has(map.index)
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between gap-2 pl-6 text-xs">
|
||||
<div class="mt-2 flex items-center justify-between gap-2 pl-6 text-sm">
|
||||
<span
|
||||
class="min-w-0 truncate font-semibold"
|
||||
style="color:{winner ? SIDE_COLORS[winner.color].text : '#a1a1aa'}"
|
||||
>{winner?.name ?? 'Draw'}{winner ? ' won' : ''}</span
|
||||
>
|
||||
<span class="shrink-0 font-mono text-[11px] tabular-nums"
|
||||
<span class="shrink-0 font-mono text-sm tabular-nums"
|
||||
><span style="color:{SIDE_COLORS.red.text}"
|
||||
>{formatScore(red, view.rules.winCondition)}</span
|
||||
><span class="px-1 text-zinc-600">–</span><span
|
||||
@@ -82,12 +82,12 @@
|
||||
{#if open.has(map.index)}
|
||||
<div transition:slide={{ duration: 180 }} class="border-t border-white/5 px-3 py-2">
|
||||
{#if map.scores.length}
|
||||
<p class="mb-1 text-[10px] font-semibold tracking-wide text-zinc-500 uppercase">
|
||||
<p class="mb-1 text-xs font-semibold tracking-wide text-zinc-400 uppercase">
|
||||
Player scores
|
||||
</p>
|
||||
{#each map.scores as score (score.playerId)}
|
||||
{@const side = live.side(score.side)}
|
||||
<div class="flex items-center gap-2 border-t border-white/5 py-1.5 text-xs">
|
||||
<div class="flex items-center gap-2 border-t border-white/5 py-1.5 text-sm">
|
||||
<span
|
||||
class="size-2 shrink-0 rounded-full"
|
||||
style="background:{side ? SIDE_COLORS[side.color].solid : '#71717a'}"
|
||||
@@ -96,7 +96,7 @@
|
||||
<a
|
||||
href="/users/{score.userId}"
|
||||
class="block truncate font-medium hover:underline">{name(score.playerId)}</a
|
||||
><span class="text-[10px] text-zinc-500"
|
||||
><span class="text-xs text-zinc-400"
|
||||
>{score.accuracy ? `${score.accuracy.toFixed(2)}%` : '–'} · {score.combo
|
||||
? `${score.combo}x`
|
||||
: '–'} · {score.mods.filter((mod) => mod !== 'NF').join('') ||
|
||||
@@ -110,15 +110,15 @@
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-xs text-zinc-500">
|
||||
<p class="text-sm text-zinc-400">
|
||||
No per-player scores were recorded for this map.
|
||||
</p>
|
||||
{/if}
|
||||
{#if isReferee}
|
||||
<div
|
||||
class="mt-2 flex flex-wrap items-center gap-1.5 border-t border-white/5 pt-2 text-xs"
|
||||
class="mt-2 flex flex-wrap items-center gap-1.5 border-t border-white/5 pt-2 text-sm"
|
||||
>
|
||||
<span class="w-full text-zinc-500">Correct winner:</span>
|
||||
<span class="w-full text-zinc-400">Correct winner:</span>
|
||||
{#each view.sides as side (side.id)}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import Avatar from '#lib/components/Avatar.svelte';
|
||||
import Disc3 from '@lucide/svelte/icons/disc-3';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { SlotView } from '$lib/match-view';
|
||||
import type { SlotView } from '#lib/match-view.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import MapCard from './MapCard.svelte';
|
||||
import { CATEGORY_NAMES, categoryRank, modColor } from './theme';
|
||||
@@ -62,13 +62,13 @@
|
||||
<span>
|
||||
<h2 class="text-sm font-semibold tracking-wide text-zinc-200 uppercase">Map pool</h2>
|
||||
{#if view.pool}
|
||||
<span class="block text-xs text-zinc-500 group-hover:text-zinc-300"
|
||||
<span class="block text-sm text-zinc-400 group-hover:text-zinc-300"
|
||||
>{view.pool.name}{view.pool.owner ? ` · from ${view.pool.owner.name}` : ''}</span
|
||||
>
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
<div class="flex gap-3 text-[11px] text-zinc-500">
|
||||
<div class="flex gap-3 text-sm text-zinc-400">
|
||||
<span><b class="text-zinc-300">{counts.open}</b> open</span>
|
||||
<span><b class="text-zinc-300">{counts.banned}</b> banned</span>
|
||||
<span><b class="text-zinc-300">{counts.played}</b> played</span>
|
||||
@@ -89,7 +89,7 @@
|
||||
<span class="font-mono text-sm font-black" style="color:{modColor(category)}"
|
||||
>{category}</span
|
||||
>
|
||||
<span class="hidden text-[10px] leading-tight text-zinc-500 sm:block"
|
||||
<span class="hidden text-xs leading-tight text-zinc-400 sm:block"
|
||||
>{CATEGORY_NAMES[category] ?? ''}</span
|
||||
>
|
||||
</div>
|
||||
@@ -101,9 +101,9 @@
|
||||
{#each slots as slot (slot.id)}
|
||||
{#if slot.status === 'live'}
|
||||
<div
|
||||
class="flex items-center justify-center gap-2 rounded-xl border-2 border-dashed text-center text-xs font-semibold {compact
|
||||
class="flex items-center justify-center gap-2 rounded-xl border-2 border-dashed text-center text-sm font-semibold {compact
|
||||
? 'h-[68px]'
|
||||
: 'h-[92px]'}"
|
||||
: 'h-[112px]'}"
|
||||
style="border-color:{modColor(slot.category)}66;color:{modColor(slot.category)}"
|
||||
>
|
||||
<Radio class="size-4 animate-pulse" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Tween } from 'svelte/motion';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { onMount } from 'svelte';
|
||||
import type { MapView } from '$lib/match-view';
|
||||
import type { MapView } from '#lib/match-view.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS, formatScore, modColor } from './theme';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { MapView, MatchView } from '$lib/match-view';
|
||||
import type { MapView, MatchView } from '#lib/match-view.js';
|
||||
import { MatchLive } from './live.svelte';
|
||||
import Scoreboard from './Scoreboard.svelte';
|
||||
import MapPool from './MapPool.svelte';
|
||||
@@ -15,10 +15,10 @@
|
||||
import NowPlaying from './NowPlaying.svelte';
|
||||
import StageFinal from './StageFinal.svelte';
|
||||
import MapReveal from './MapReveal.svelte';
|
||||
import PredictionCard from '$lib/components/PredictionCard.svelte';
|
||||
import type { PredictionResult, PredictionState } from '$lib/predictions';
|
||||
import PredictionCard from '#lib/components/PredictionCard.svelte';
|
||||
import type { PredictionResult, PredictionState } from '#lib/predictions.js';
|
||||
import { blinkTitle, chime, notify } from './attention';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import Bell from '@lucide/svelte/icons/bell';
|
||||
import BellOff from '@lucide/svelte/icons/bell-off';
|
||||
@@ -174,7 +174,10 @@
|
||||
};
|
||||
if (v.phase === 'playing')
|
||||
return v.live?.startedAt
|
||||
? { label: 'Map in progress', detail: 'Scores and progress update as players finish.' }
|
||||
? {
|
||||
label: 'Map in progress',
|
||||
detail: 'Follow live scores, accuracy and combo while the map plays.'
|
||||
}
|
||||
: { label: 'Ready up in osu!', detail: 'The selected map is loading. Get ready to play.' };
|
||||
return { label: 'Match in progress', detail: 'Updates appear here as they happen.' };
|
||||
});
|
||||
@@ -195,16 +198,19 @@
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto flex max-w-[1600px] flex-col gap-3 lg:h-[calc(100dvh-6.5rem)] lg:min-h-[680px]">
|
||||
<header class="flex min-h-10 flex-wrap items-center gap-2">
|
||||
<div
|
||||
data-match-dashboard
|
||||
class="mx-auto flex max-w-[1680px] flex-col gap-3 lg:h-[calc(100dvh-6.5rem)] lg:min-h-[680px]"
|
||||
>
|
||||
<header class="flex min-h-10 shrink-0 flex-wrap items-center gap-2">
|
||||
<a
|
||||
href="/matches"
|
||||
class="grid size-9 place-items-center rounded-lg border border-white/10 text-zinc-400 transition hover:bg-white/5 hover:text-white"
|
||||
aria-label="All matches"><ArrowLeft class="size-4" /></a
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h1 class="truncate text-base font-bold tracking-tight sm:text-lg">{v.name ?? title}</h1>
|
||||
<div class="flex flex-wrap items-center gap-x-2 text-xs text-zinc-500">
|
||||
<h1 class="truncate text-lg font-bold tracking-tight sm:text-xl">{v.name ?? title}</h1>
|
||||
<div class="flex flex-wrap items-center gap-x-2 text-sm text-zinc-400">
|
||||
{#if v.tournament}
|
||||
<a
|
||||
href="/tournaments/{v.tournament.id}"
|
||||
@@ -219,7 +225,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="hidden items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium sm:flex {decided ||
|
||||
class="hidden items-center gap-1.5 rounded-full border px-2.5 py-1 text-sm font-medium sm:flex {decided ||
|
||||
live.connected
|
||||
? 'border-emerald-400/20 bg-emerald-400/5 text-emerald-300'
|
||||
: 'border-amber-400/30 bg-amber-400/10 text-amber-300'}"
|
||||
@@ -261,11 +267,11 @@
|
||||
</Button>
|
||||
<details class="relative z-20">
|
||||
<summary
|
||||
class="flex h-8 cursor-pointer list-none items-center gap-1 rounded-lg border border-white/10 px-2.5 text-xs text-zinc-300 hover:bg-white/5"
|
||||
class="flex h-8 cursor-pointer list-none items-center gap-1 rounded-lg border border-white/10 px-2.5 text-sm text-zinc-300 hover:bg-white/5"
|
||||
>Details <ChevronDown class="size-3" /></summary
|
||||
>
|
||||
<div
|
||||
class="absolute right-0 mt-2 w-64 rounded-xl border border-white/10 bg-zinc-900 p-3 text-xs shadow-2xl"
|
||||
class="absolute right-0 mt-2 w-64 rounded-xl border border-white/10 bg-zinc-900 p-3 text-sm shadow-2xl"
|
||||
>
|
||||
<p class="mb-2 font-semibold text-white">Match details</p>
|
||||
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-zinc-400">
|
||||
@@ -301,14 +307,14 @@
|
||||
|
||||
<Scoreboard {live} />
|
||||
|
||||
<div class="grid min-h-0 flex-1 gap-3 lg:grid-cols-[minmax(0,1fr)_23rem]">
|
||||
<div class="grid min-h-0 flex-1 gap-3 lg:grid-cols-[minmax(0,1fr)_26rem]">
|
||||
<section
|
||||
class="relative flex min-h-[25rem] flex-col overflow-hidden rounded-2xl border border-white/10 bg-gradient-to-b from-zinc-900/90 to-zinc-950 lg:min-h-0"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-2 border-b border-white/8 px-4 py-3 sm:px-5"
|
||||
class="flex shrink-0 flex-wrap items-center justify-between gap-2 border-b border-white/8 px-4 py-3 sm:px-5"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-xs font-semibold text-zinc-300">
|
||||
<div class="flex items-center gap-2 text-base font-semibold text-zinc-200">
|
||||
<span
|
||||
class="size-2 rounded-full {live.myTurn
|
||||
? 'animate-pulse bg-amber-300'
|
||||
@@ -318,18 +324,20 @@
|
||||
<nav aria-label="Match progress" class="flex items-center gap-1.5">
|
||||
{#each steps as step, index}
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-[10px] font-semibold {index === activeStep
|
||||
class="rounded-full px-2 py-0.5 text-xs font-semibold {index === activeStep
|
||||
? 'bg-white/10 text-white'
|
||||
: index < activeStep
|
||||
? 'text-emerald-300'
|
||||
: 'text-zinc-600'}">{index < activeStep ? '✓ ' : ''}{step}</span
|
||||
: 'text-zinc-500'}">{index < activeStep ? '✓ ' : ''}{step}</span
|
||||
>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto px-4 py-4 sm:px-6 sm:py-5">
|
||||
<div data-match-stage class="flex min-h-0 flex-1 flex-col overflow-y-auto px-4 py-3 sm:px-5">
|
||||
<div class="my-auto w-full">
|
||||
<p class="mb-4 text-center text-xs text-zinc-500">{guidance.detail}</p>
|
||||
{#if v.phase !== 'playing'}<p class="mb-3 text-center text-base text-zinc-300">
|
||||
{guidance.detail}
|
||||
</p>{/if}
|
||||
{#if v.paused && !decided}
|
||||
<div
|
||||
transition:fade
|
||||
@@ -361,8 +369,8 @@
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-white/8 bg-black/20 px-3 py-2.5 sm:px-4">
|
||||
<div class="mb-2 flex items-center justify-between text-[11px] font-semibold text-zinc-400">
|
||||
<div class="shrink-0 border-t border-white/8 bg-black/20 px-3 py-2.5 sm:px-4">
|
||||
<div class="mb-2 flex items-center justify-between text-sm font-semibold text-zinc-400">
|
||||
<span>MAP JOURNEY</span><span>{v.maps.length} played · first to {v.rules.winsNeeded}</span
|
||||
>
|
||||
</div>
|
||||
@@ -373,14 +381,14 @@
|
||||
{@const slot = map ? live.slot(map.slotId) : isLive ? live.slot(v.live?.slotId) : null}
|
||||
{@const winner = map ? live.side(map.winner) : null}
|
||||
<div
|
||||
class="flex min-w-24 flex-1 basis-24 items-center gap-2 rounded-lg border px-2 py-1.5 text-xs {isLive
|
||||
class="flex min-w-24 flex-1 basis-24 items-center gap-2 rounded-lg border px-2 py-1.5 text-sm {isLive
|
||||
? 'border-emerald-400/40 bg-emerald-400/10'
|
||||
: map
|
||||
? 'border-white/12 bg-white/5'
|
||||
: 'border-white/5 bg-white/[0.02] text-zinc-600'}"
|
||||
: 'border-white/5 bg-white/[0.02] text-zinc-500'}"
|
||||
>
|
||||
<span
|
||||
class="grid size-6 shrink-0 place-items-center rounded-md bg-black/30 font-mono text-[10px] font-bold"
|
||||
class="grid size-6 shrink-0 place-items-center rounded-md bg-black/30 font-mono text-xs font-bold"
|
||||
style={slot ? `color:${modColor(slot.category)}` : ''}
|
||||
>{slot?.label ?? index + 1}</span
|
||||
>
|
||||
@@ -416,10 +424,10 @@
|
||||
role="tab"
|
||||
aria-selected={panel === 'activity'}
|
||||
onclick={() => (panel = 'activity')}
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-xs font-semibold transition {panel ===
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-sm font-semibold transition {panel ===
|
||||
'activity'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-zinc-500 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
: 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
><List class="size-3.5" />Activity</button
|
||||
>
|
||||
<button
|
||||
@@ -427,10 +435,10 @@
|
||||
role="tab"
|
||||
aria-selected={panel === 'pool'}
|
||||
onclick={() => (panel = 'pool')}
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-xs font-semibold transition {panel ===
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-sm font-semibold transition {panel ===
|
||||
'pool'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-zinc-500 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
: 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
><Layers class="size-3.5" />Maps</button
|
||||
>
|
||||
<button
|
||||
@@ -438,10 +446,10 @@
|
||||
role="tab"
|
||||
aria-selected={panel === 'results'}
|
||||
onclick={() => (panel = 'results')}
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-xs font-semibold transition {panel ===
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-sm font-semibold transition {panel ===
|
||||
'results'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-zinc-500 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
: 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
><Trophy class="size-3.5" />Results</button
|
||||
>
|
||||
{#if prediction}<button
|
||||
@@ -449,10 +457,10 @@
|
||||
role="tab"
|
||||
aria-selected={panel === 'predictions'}
|
||||
onclick={() => (panel = 'predictions')}
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-xs font-semibold transition {panel ===
|
||||
class="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-lg px-2 py-2 text-sm font-semibold transition {panel ===
|
||||
'predictions'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-zinc-500 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
: 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'}"
|
||||
><Sparkles class="size-3.5" />Picks</button
|
||||
>{/if}
|
||||
</div>
|
||||
@@ -464,7 +472,7 @@
|
||||
{:else if panel === 'results'}
|
||||
<div class="h-full overflow-y-auto p-2">
|
||||
{#if v.maps.length}<MapHistory {live} {isReferee} embedded />{:else}<p
|
||||
class="px-3 py-12 text-center text-sm text-zinc-500"
|
||||
class="px-3 py-12 text-center text-sm text-zinc-400"
|
||||
>
|
||||
Map results will appear here after the first play.
|
||||
</p>{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||
import type { LogLine } from '$lib/match-view';
|
||||
import * as Tabs from '#lib/components/ui/tabs/index.js';
|
||||
import type { LogLine } from '#lib/match-view.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS } from './theme';
|
||||
import Ban from '@lucide/svelte/icons/ban';
|
||||
@@ -66,9 +66,9 @@
|
||||
{#each events as line, index (line.t + ':' + line.text + ':' + index)}
|
||||
{@const Icon = icons[line.kind] ?? Info}
|
||||
{@const side = live.side(line.side)}
|
||||
<li class="flex gap-2 text-[13px] leading-snug {index === 0 ? 'vash-rise' : ''}">
|
||||
<li class="flex gap-2 text-base leading-snug {index === 0 ? 'vash-rise' : ''}">
|
||||
<span
|
||||
class="mt-0.5 grid size-5 shrink-0 place-items-center rounded-md"
|
||||
class="mt-0.5 grid size-6 shrink-0 place-items-center rounded-md"
|
||||
style="background:{side
|
||||
? SIDE_COLORS[side.color].soft
|
||||
: 'rgb(255 255 255 / 0.06)'};color:{side
|
||||
@@ -77,12 +77,12 @@
|
||||
? '#fcd34d'
|
||||
: '#a1a1aa'}"
|
||||
>
|
||||
<Icon class="size-3" />
|
||||
<Icon class="size-3.5" />
|
||||
</span>
|
||||
<span class="flex-1 {line.kind === 'warn' ? 'text-amber-200' : 'text-zinc-300'}"
|
||||
>{line.text}</span
|
||||
>
|
||||
<span class="shrink-0 font-mono text-[10px] text-zinc-600">{time(line.t)}</span>
|
||||
<span class="shrink-0 font-mono text-xs text-zinc-400">{time(line.t)}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
@@ -92,14 +92,14 @@
|
||||
bind:this={chatBox}
|
||||
onscroll={() =>
|
||||
chatBox && (stick = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight < 40)}
|
||||
class="h-full max-h-[28rem] overflow-y-auto px-3 pb-3 font-mono text-[12px] leading-relaxed lg:max-h-none"
|
||||
class="h-full max-h-[28rem] overflow-y-auto px-3 pb-3 font-mono text-sm leading-relaxed lg:max-h-none"
|
||||
>
|
||||
{#if view.chat.length === 0}
|
||||
<p class="py-8 text-center font-sans text-sm text-zinc-500">No messages yet.</p>
|
||||
{/if}
|
||||
{#each view.chat as line, index (line.t + ':' + index)}
|
||||
<div class="break-words">
|
||||
<span class="text-zinc-600">{time(line.t)}</span>
|
||||
<span class="text-zinc-400">{time(line.t)}</span>
|
||||
<span
|
||||
class="font-semibold {line.role === 'bancho'
|
||||
? 'text-pink-400'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import MapCard from './MapCard.svelte';
|
||||
import { SIDE_COLORS, formatScore, modColor, playerHref } from './theme';
|
||||
import { liveTotal, scoreIsFresh } from '#lib/match-scores.js';
|
||||
import { receiveCard, sendCard } from './transitions';
|
||||
import Radio from '@lucide/svelte/icons/radio';
|
||||
import Hourglass from '@lucide/svelte/icons/hourglass';
|
||||
@@ -18,15 +19,11 @@
|
||||
const speed = $derived(
|
||||
slot.mods.includes('DT') || slot.mods.includes('NC') ? 1.5 : slot.mods.includes('HT') ? 0.75 : 1
|
||||
);
|
||||
|
||||
const players = $derived(
|
||||
view.sides
|
||||
.flatMap((side) => side.players.map((player) => ({ player, side })))
|
||||
.filter(({ player }) => player.live)
|
||||
);
|
||||
const reported = $derived(
|
||||
Math.max(0, ...players.map(({ player }) => player.live?.progress ?? 0))
|
||||
);
|
||||
const players = $derived(view.sides.flatMap((side) => side.players));
|
||||
const reporting = $derived(players.filter((player) => player.live));
|
||||
const fresh = $derived(reporting.filter((player) => scoreIsFresh(player.live, live.now)));
|
||||
const stale = $derived(reporting.length > 0 && fresh.length === 0);
|
||||
const reported = $derived(Math.max(0, ...reporting.map((player) => player.live?.progress ?? 0)));
|
||||
const progress = $derived(
|
||||
!started
|
||||
? 0
|
||||
@@ -34,53 +31,60 @@
|
||||
? reported
|
||||
: Math.min(1, (live.now - current.startedAt!) / (((slot.lengthSec ?? 180) * 1000) / speed))
|
||||
);
|
||||
const totals = $derived(
|
||||
view.sides.map((side) =>
|
||||
side.players.reduce((sum, player) => sum + (player.live?.score ?? 0), 0)
|
||||
const totals = $derived(view.sides.map((side) => liveTotal(side, view.rules.winCondition)));
|
||||
const values = [
|
||||
new Tween(0, { duration: 250, easing: cubicOut }),
|
||||
new Tween(0, { duration: 250, easing: cubicOut })
|
||||
];
|
||||
$effect(() => {
|
||||
values[0].target = totals[0] ?? 0;
|
||||
values[1].target = totals[1] ?? 0;
|
||||
});
|
||||
const both = $derived(totals[0] != null && totals[1] != null);
|
||||
const share = $derived(
|
||||
both && values[0].current + values[1].current > 0
|
||||
? values[0].current / (values[0].current + values[1].current)
|
||||
: 0.5
|
||||
);
|
||||
const complete = $derived(
|
||||
view.sides.every(
|
||||
(side) =>
|
||||
side.players.filter((player) => scoreIsFresh(player.live, live.now)).length >=
|
||||
view.rules.teamSize
|
||||
)
|
||||
);
|
||||
const red = new Tween(0, { duration: 700, easing: cubicOut });
|
||||
const blue = new Tween(0, { duration: 700, easing: cubicOut });
|
||||
$effect(() => {
|
||||
red.target = totals[0];
|
||||
blue.target = totals[1];
|
||||
});
|
||||
const share = $derived(
|
||||
red.current + blue.current > 0 ? red.current / (red.current + blue.current) : 0.5
|
||||
);
|
||||
const leading = $derived(
|
||||
totals[0] === totals[1] ? null : totals[0] > totals[1] ? view.sides[0] : view.sides[1]
|
||||
complete && both && totals[0] !== totals[1] ? view.sides[totals[0]! > totals[1]! ? 0 : 1] : null
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold">
|
||||
<div class="flex flex-col gap-2" data-match-playing>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 text-base">
|
||||
<div class="flex items-center gap-2 font-semibold">
|
||||
{#if started}
|
||||
<span class="flex items-center gap-1.5 text-emerald-300"
|
||||
><Radio class="size-4 animate-pulse" /> Live</span
|
||||
>
|
||||
{:else if starting}
|
||||
<span class="text-amber-300">Starting…</span>
|
||||
<span class={stale || !reporting.length ? 'text-amber-300' : 'text-emerald-300'}>
|
||||
<Radio class="mr-1.5 inline size-4" />{stale
|
||||
? 'Score feed interrupted'
|
||||
: reporting.length
|
||||
? 'Live scores'
|
||||
: 'Waiting for live scores'}
|
||||
</span>
|
||||
{:else if starting}<span class="text-amber-300">Starting…</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-1.5 text-zinc-300"
|
||||
><Hourglass class="size-4" /> Ready up in osu!</span
|
||||
<span class="text-zinc-200"><Hourglass class="mr-1.5 inline size-4" />Ready up in osu!</span
|
||||
>
|
||||
{#if view.deadlineAt}<span class="text-xs text-zinc-500"
|
||||
{#if view.deadlineAt}<span class="text-sm text-zinc-400"
|
||||
>force start in {live.secondsLeft()}s</span
|
||||
>{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400">
|
||||
{#if current.tiebreaker}
|
||||
<span class="font-bold text-white">Tiebreaker</span>
|
||||
{:else if picker}
|
||||
Picked by <span class="font-semibold" style="color:{SIDE_COLORS[picker.color].text}"
|
||||
>{picker.name}</span
|
||||
>
|
||||
{/if}
|
||||
{#if current.attempt > 1}<span class="ml-2 rounded bg-amber-400/15 px-1.5 text-amber-300"
|
||||
>replay #{current.attempt}</span
|
||||
<div class="text-sm text-zinc-300">
|
||||
{#if current.tiebreaker}<span class="font-bold text-white">Tiebreaker</span>
|
||||
{:else if picker}Picked by <span
|
||||
class="font-semibold"
|
||||
style="color:{SIDE_COLORS[picker.color].text}">{picker.name}</span
|
||||
>{/if}
|
||||
{#if current.attempt > 1}<span class="ml-2 text-amber-300">replay #{current.attempt}</span
|
||||
>{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,61 +102,87 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1.5 text-[11px]">
|
||||
{#each slot.mods as mod (mod)}<span class="rounded bg-white/10 px-1.5 py-0.5 font-semibold"
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
{#each slot.mods as mod (mod)}<span class="rounded bg-white/10 px-2 py-0.5 font-semibold"
|
||||
>{mod}</span
|
||||
>{/each}
|
||||
{#if slot.freemod}<span
|
||||
class="rounded bg-emerald-400/15 px-1.5 py-0.5 font-semibold text-emerald-300">Freemod</span
|
||||
class="rounded bg-emerald-400/15 px-2 py-0.5 font-semibold text-emerald-300">Freemod</span
|
||||
>{/if}
|
||||
<span class="rounded bg-white/5 px-1.5 py-0.5 text-zinc-400"
|
||||
>{view.rules.winCondition.replace('_', ' ').toUpperCase()}</span
|
||||
<span class="text-zinc-400"
|
||||
>{view.rules.winCondition.replace('_', ' ').toUpperCase()} · provisional scores</span
|
||||
>
|
||||
{#if started}<span class="ml-auto text-zinc-400 tabular-nums"
|
||||
>{fresh.length}/{view.rules.teamSize * 2} players reporting</span
|
||||
>{/if}
|
||||
</div>
|
||||
|
||||
{#if players.length}
|
||||
<div class="space-y-2">
|
||||
<div class="flex h-9 overflow-hidden rounded-lg text-xs font-bold">
|
||||
<div
|
||||
class="flex items-center justify-start px-2.5 transition-all duration-700"
|
||||
style="width:{share * 100}%;background:{SIDE_COLORS.red.solid}"
|
||||
{#if started}
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
{#each view.sides as side, index (side.id)}
|
||||
{@const colors = SIDE_COLORS[side.color]}
|
||||
<section
|
||||
class="min-w-0 overflow-hidden rounded-xl border"
|
||||
style="border-color:{colors.ring};background:{colors.soft}"
|
||||
aria-label="{side.name} live scores"
|
||||
>
|
||||
{formatScore(red.current, view.rules.winCondition)}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-1 items-center justify-end px-2.5"
|
||||
style="background:{SIDE_COLORS.blue.solid}"
|
||||
>
|
||||
{formatScore(blue.current, view.rules.winCondition)}
|
||||
</div>
|
||||
</div>
|
||||
{#if leading}
|
||||
<p class="text-center text-xs text-zinc-400">
|
||||
<span style="color:{SIDE_COLORS[leading.color].text}" class="font-semibold"
|
||||
>{leading.name}</span
|
||||
>
|
||||
leads by
|
||||
{formatScore(Math.abs(totals[0] - totals[1]), view.rules.winCondition)}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="grid gap-1.5 sm:grid-cols-2">
|
||||
{#each players as { player, side } (player.id)}
|
||||
<div class="flex items-center gap-2 rounded-lg bg-white/5 px-2 py-1.5 text-xs">
|
||||
<span class="h-6 w-1 rounded" style="background:{SIDE_COLORS[side.color].solid}"></span>
|
||||
<a href={playerHref(player)} class="flex-1 truncate hover:underline"
|
||||
>{player.osuName ?? player.name}</a
|
||||
<header class="flex items-center justify-between gap-3 px-3 py-2">
|
||||
<span class="min-w-0 truncate text-base font-semibold" style="color:{colors.text}"
|
||||
>{side.name}</span
|
||||
>
|
||||
<span class="text-zinc-400 tabular-nums">{player.live!.accuracy.toFixed(2)}%</span>
|
||||
<span class="text-zinc-400 tabular-nums">{player.live!.combo}x</span>
|
||||
<span class="w-20 text-right font-semibold tabular-nums"
|
||||
>{player.live!.score.toLocaleString('en-US')}</span
|
||||
<span class="shrink-0 text-2xl font-black tabular-nums xl:text-3xl" data-team-live-score
|
||||
>{totals[index] == null
|
||||
? '—'
|
||||
: formatScore(values[index].current, view.rules.winCondition)}</span
|
||||
>
|
||||
</header>
|
||||
<div class="bg-black/25">
|
||||
{#each side.players as player (player.id)}
|
||||
{@const freshPlayer = scoreIsFresh(player.live, live.now)}
|
||||
<div class="flex min-h-14 items-center gap-3 border-t border-white/5 px-3 py-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<a
|
||||
href={playerHref(player)}
|
||||
class="block truncate text-base font-semibold hover:underline"
|
||||
>{player.osuName ?? player.name}</a
|
||||
>
|
||||
<div
|
||||
class="text-sm tabular-nums {freshPlayer ? 'text-zinc-300' : 'text-amber-200'}"
|
||||
>
|
||||
{#if player.live}{player.live.accuracy.toFixed(2)}% · {player.live
|
||||
.combo}x{#if !freshPlayer}
|
||||
· delayed{/if}
|
||||
{:else}Waiting for score{/if}
|
||||
</div>
|
||||
</div>
|
||||
<span class="shrink-0 text-lg font-bold tabular-nums" data-player-live-score
|
||||
>{player.live ? player.live.score.toLocaleString('en-US') : '—'}</span
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if started}
|
||||
<p class="text-center text-xs text-zinc-500">Scores appear the moment the map ends.</p>
|
||||
{#if both}
|
||||
<div class="flex h-2 overflow-hidden rounded-full bg-white/5" aria-hidden="true">
|
||||
<div
|
||||
class="transition-[width] duration-250"
|
||||
style="width:{share * 100}%;background:{SIDE_COLORS.red.solid}"
|
||||
></div>
|
||||
<div class="flex-1" style="background:{SIDE_COLORS.blue.solid}"></div>
|
||||
</div>
|
||||
{/if}
|
||||
<p class="text-center text-sm text-zinc-300">
|
||||
{#if stale}Showing the last received scores. Waiting for the score feed to reconnect.
|
||||
{:else if !reporting.length}Waiting for live score updates. Final results still arrive when
|
||||
the map ends.
|
||||
{:else if leading}<span class="font-semibold" style="color:{SIDE_COLORS[leading.color].text}"
|
||||
>{leading.name}</span
|
||||
>
|
||||
leads by {formatScore(Math.abs(totals[0]! - totals[1]!), view.rules.winCondition)}
|
||||
{:else if complete}Scores are tied.
|
||||
{:else}Waiting for the remaining players’ live scores.{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as Sheet from '$lib/components/ui/sheet/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import * as Sheet from '#lib/components/ui/sheet/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS } from './theme';
|
||||
import Shield from '@lucide/svelte/icons/shield';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { PHASE_LABELS, type SideView } from '$lib/match-view';
|
||||
import { PHASE_LABELS, type SideView } from '#lib/match-view.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS, flagUrl, initials, playerHref, sideHref } from './theme';
|
||||
import CountdownRing from './CountdownRing.svelte';
|
||||
@@ -79,11 +79,11 @@
|
||||
<div class="min-w-0 flex-1">
|
||||
<a
|
||||
href={sideHref(side)}
|
||||
class="block truncate text-base font-bold tracking-tight hover:underline sm:text-xl"
|
||||
class="block truncate text-lg font-bold tracking-tight hover:underline sm:text-2xl"
|
||||
>{side.name}</a
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-zinc-400 {align ===
|
||||
class="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-zinc-400 {align ===
|
||||
'right'
|
||||
? 'justify-end'
|
||||
: ''}"
|
||||
@@ -159,26 +159,26 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<section class="flex items-stretch gap-2 sm:gap-3">
|
||||
<section class="flex shrink-0 items-stretch gap-2 sm:gap-3">
|
||||
{@render team(red, 'left')}
|
||||
<div class="flex w-28 shrink-0 flex-col items-center justify-center gap-1.5 sm:w-44">
|
||||
<div class="flex w-28 shrink-0 flex-col items-center justify-center gap-1.5 sm:w-56">
|
||||
<div class="flex items-baseline gap-2 font-black tabular-nums sm:gap-3">
|
||||
{#key red.points}<span
|
||||
class="vash-pop text-4xl sm:text-6xl"
|
||||
class="vash-pop text-4xl sm:text-7xl"
|
||||
style="color:{SIDE_COLORS.red.text}">{red.points}</span
|
||||
>{/key}
|
||||
<span class="text-xl text-zinc-600 sm:text-3xl">:</span>
|
||||
{#key blue.points}<span
|
||||
class="vash-pop text-4xl sm:text-6xl"
|
||||
class="vash-pop text-4xl sm:text-7xl"
|
||||
style="color:{SIDE_COLORS.blue.text}">{blue.points}</span
|
||||
>{/key}
|
||||
</div>
|
||||
<div class="text-[11px] tracking-wide text-zinc-500 uppercase">
|
||||
<div class="text-sm tracking-wide text-zinc-400 uppercase">
|
||||
First to {needed} · BO{view.rules.bestOf}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-semibold {view.paused
|
||||
class="flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-sm font-semibold {view.paused
|
||||
? 'border-amber-400/40 bg-amber-400/10 text-amber-300'
|
||||
: liveNow
|
||||
? 'border-emerald-400/40 bg-emerald-400/10 text-emerald-300'
|
||||
@@ -193,7 +193,7 @@
|
||||
deadline={view.deadlineAt}
|
||||
now={live.now}
|
||||
total={totalMs}
|
||||
size={36}
|
||||
size={52}
|
||||
/>{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import * as Sheet from '$lib/components/ui/sheet/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
import * as Sheet from '#lib/components/ui/sheet/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import { Switch } from '#lib/components/ui/switch/index.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS } from './theme';
|
||||
import FlaskConical from '@lucide/svelte/icons/flask-conical';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS, modColor, sideHref } from './theme';
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import Avatar from '#lib/components/Avatar.svelte';
|
||||
import Trophy from '@lucide/svelte/icons/trophy';
|
||||
import CircleX from '@lucide/svelte/icons/circle-x';
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
|
||||
<div class="flex flex-col items-center gap-4 py-2 text-center">
|
||||
{#if cancelled}
|
||||
<CircleX class="size-14 text-zinc-500" />
|
||||
<CircleX class="size-14 text-zinc-400" />
|
||||
<h3 class="text-2xl font-black">Match cancelled</h3>
|
||||
<p class="max-w-md text-sm text-zinc-400">{view.endReason ?? 'This match was cancelled.'}</p>
|
||||
<p class="max-w-md text-base text-zinc-400">{view.endReason ?? 'This match was cancelled.'}</p>
|
||||
{:else if winner}
|
||||
{@const colors = SIDE_COLORS[winner.color]}
|
||||
{@const loser = view.sides.find((side) => side.id !== winner.id)!}
|
||||
<div class="text-xs font-bold tracking-[0.3em] text-zinc-400 uppercase">Victory</div>
|
||||
<div class="text-sm font-bold tracking-[0.3em] text-zinc-400 uppercase">Victory</div>
|
||||
<div class="flex items-center gap-6 sm:gap-10">
|
||||
{#each [winner, loser] as side (side.id)}
|
||||
{@const sideColors = SIDE_COLORS[side.color]}
|
||||
@@ -90,7 +90,7 @@
|
||||
{@const slot = live.slot(map.slotId)}
|
||||
{@const side = live.side(map.winner)}
|
||||
<span
|
||||
class="flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2 py-1 text-xs"
|
||||
class="flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2 py-1 text-sm"
|
||||
>
|
||||
<span class="font-mono font-bold" style="color:{slot ? modColor(slot.category) : '#fff'}"
|
||||
>{slot?.label}</span
|
||||
@@ -104,6 +104,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if view.phase === 'closing'}
|
||||
<p class="text-xs text-zinc-500">The lobby closes shortly. GG!</p>
|
||||
<p class="text-sm text-zinc-400">The lobby closes shortly. GG!</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS, flagUrl, initials, playerHref, sideHref } from './theme';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import CheckCircle from '@lucide/svelte/icons/circle-check';
|
||||
import Loader from '@lucide/svelte/icons/loader-circle';
|
||||
import Send from '@lucide/svelte/icons/send';
|
||||
@@ -15,8 +15,8 @@
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="text-center">
|
||||
{#if created}
|
||||
<h3 class="text-xl font-bold">Getting a lobby ready</h3>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
<h3 class="text-2xl font-bold">Getting a lobby ready</h3>
|
||||
<p class="mt-1 text-base text-zinc-400">
|
||||
{view.scheduledAt && Date.parse(view.scheduledAt) > live.now
|
||||
? `The lobby opens at ${new Date(view.scheduledAt).toLocaleString()}.`
|
||||
: 'Waiting for a free osu! lobby slot. This usually takes seconds.'}
|
||||
@@ -30,8 +30,8 @@
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<h3 class="text-xl font-bold">Join the lobby</h3>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
<h3 class="text-2xl font-bold">Join the lobby</h3>
|
||||
<p class="mt-1 text-base text-zinc-400">
|
||||
Accept the invite in osu! (check your chat). Each team needs {view.rules.teamSize} player{view
|
||||
.rules.teamSize === 1
|
||||
? ''
|
||||
@@ -65,7 +65,7 @@
|
||||
<a href={sideHref(side)} class="font-semibold hover:underline" style="color:{colors.text}"
|
||||
>{side.name}</a
|
||||
>
|
||||
<span class="text-xs text-zinc-400 tabular-nums"
|
||||
<span class="text-sm text-zinc-400 tabular-nums"
|
||||
>{Math.min(present, view.rules.teamSize)}/{view.rules.teamSize}</span
|
||||
>
|
||||
</div>
|
||||
@@ -80,13 +80,13 @@
|
||||
<img
|
||||
src={player.avatarUrl}
|
||||
alt=""
|
||||
class="size-7 rounded-full object-cover {player.inLobby
|
||||
class="size-8 rounded-full object-cover {player.inLobby
|
||||
? ''
|
||||
: 'opacity-50 grayscale'}"
|
||||
/>
|
||||
{:else}
|
||||
<span
|
||||
class="grid size-7 place-items-center rounded-full bg-zinc-800 text-[10px] font-bold"
|
||||
class="grid size-8 place-items-center rounded-full bg-zinc-800 text-[10px] font-bold"
|
||||
>{initials(player.name)}</span
|
||||
>
|
||||
{/if}
|
||||
@@ -97,14 +97,14 @@
|
||||
/>{/if}
|
||||
<a
|
||||
href={playerHref(player)}
|
||||
class="flex-1 truncate text-sm hover:underline {player.inLobby
|
||||
class="flex-1 truncate text-base hover:underline {player.inLobby
|
||||
? 'text-white'
|
||||
: 'text-zinc-400'}">{player.osuName ?? player.name}</a
|
||||
>
|
||||
{#if player.inLobby}
|
||||
<CheckCircle class="size-4 text-emerald-400" />
|
||||
{:else if !created}
|
||||
<Loader class="size-4 animate-spin text-zinc-500" />
|
||||
<Loader class="size-4 animate-spin text-zinc-400" />
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { SideView } from '$lib/match-view';
|
||||
import type { SideView } from '#lib/match-view.js';
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS } from './theme';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import Dice from '@lucide/svelte/icons/dices';
|
||||
|
||||
const { live }: { live: MatchLive } = $props();
|
||||
@@ -53,7 +53,7 @@
|
||||
{@const colors = SIDE_COLORS[side.color]}
|
||||
{@const won = leader?.id === side.id && !rolling[side.id]}
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="text-sm font-semibold" style="color:{colors.text}">{side.name}</div>
|
||||
<div class="text-base font-semibold" style="color:{colors.text}">{side.name}</div>
|
||||
<div
|
||||
class="relative grid size-28 place-items-center rounded-3xl border-2 transition-all duration-500 sm:size-36 {rolling[
|
||||
side.id
|
||||
@@ -75,12 +75,12 @@
|
||||
<Dice class="size-12 text-zinc-600" />
|
||||
{/if}
|
||||
{#if side.serverRoll && !rolling[side.id]}
|
||||
<span class="absolute -bottom-2 rounded bg-zinc-800 px-1.5 text-[10px] text-zinc-400"
|
||||
<span class="absolute -bottom-2 rounded bg-zinc-800 px-1.5 text-xs text-zinc-400"
|
||||
>auto-rolled</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if won}<span class="vash-pop text-xs font-bold tracking-widest text-amber-300 uppercase"
|
||||
{#if won}<span class="vash-pop text-sm font-bold tracking-widest text-amber-300 uppercase"
|
||||
>High roll</span
|
||||
>{/if}
|
||||
</div>
|
||||
@@ -88,10 +88,10 @@
|
||||
|
||||
<div class="flex flex-col items-center gap-5">
|
||||
<div class="text-center">
|
||||
<h3 class="text-xl font-bold">
|
||||
<h3 class="text-2xl font-bold">
|
||||
{view.rollFor === 'tiebreaker' ? 'Tiebreaker roll' : 'Roll for order'}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
<p class="mt-1 text-base text-zinc-400">
|
||||
{view.rollFor === 'tiebreaker'
|
||||
? 'The higher roll picks the tiebreaker.'
|
||||
: view.rules.bansPerTeam > 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { MatchLive } from './live.svelte';
|
||||
import { SIDE_COLORS, modColor } from './theme';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import Ban from '@lucide/svelte/icons/ban';
|
||||
import Crosshair from '@lucide/svelte/icons/crosshair';
|
||||
import Scale from '@lucide/svelte/icons/scale';
|
||||
@@ -50,7 +50,7 @@
|
||||
class="size-6 text-white"
|
||||
/>{:else}<Crosshair class="size-6 text-white" />{/if}
|
||||
</div>
|
||||
<div class="text-2xl font-black tracking-tight sm:text-3xl">
|
||||
<div class="text-2xl font-black tracking-tight sm:text-4xl">
|
||||
{#if mine}
|
||||
<span style="color:{colors.text}">Your turn</span>
|
||||
{:else}
|
||||
@@ -58,7 +58,7 @@
|
||||
<span class="text-zinc-300">{verb}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-sm text-zinc-400">
|
||||
<p class="text-base text-zinc-300">
|
||||
{#if kind === 'choose'}
|
||||
{actor.name} won the roll. {choiceHint}
|
||||
{:else if kind === 'ban'}
|
||||
@@ -74,7 +74,7 @@
|
||||
{#if mine}
|
||||
<Button
|
||||
size="lg"
|
||||
class="vash-glow mt-2 h-11 px-7 font-bold"
|
||||
class="vash-glow mt-2 h-12 px-7 text-lg font-bold"
|
||||
style="--glow:{colors.solid}"
|
||||
onclick={onact}
|
||||
>
|
||||
@@ -96,7 +96,7 @@
|
||||
{@const slot = live.slot(line.slotId)}
|
||||
{#if side && slot}
|
||||
<span
|
||||
class="vash-rise flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold"
|
||||
class="vash-rise flex items-center gap-1 rounded-full border px-2 py-0.5 text-sm font-semibold"
|
||||
style="border-color:{SIDE_COLORS[side.color].ring};background:{SIDE_COLORS[side.color]
|
||||
.soft}"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { MatchCommand, MatchView, SideView } from '$lib/match-view';
|
||||
import type { Crowd } from '$lib/predictions';
|
||||
import { connectTopics } from '$lib/topic-socket';
|
||||
import type { MatchCommand, MatchView, SideView } from '#lib/match-view.js';
|
||||
import type { Crowd } from '#lib/predictions.js';
|
||||
import { connectTopics } from '#lib/topic-socket.js';
|
||||
|
||||
type SimCommand = Record<string, unknown> & { type: string };
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SideColor } from '$lib/match-view';
|
||||
import type { SideColor } from '#lib/match-view.js';
|
||||
|
||||
/** One color per mod pool. Tiebreakers are white, as on every tournament sheet. */
|
||||
const MOD_COLORS: Record<string, string> = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { renderBBCode } from '$lib/bbcode';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
||||
import { renderBBCode } from '#lib/bbcode.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import { Textarea } from '#lib/components/ui/textarea/index.js';
|
||||
import Palette from '@lucide/svelte/icons/palette';
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { flagUrl, initials } from '$lib/components/match/theme';
|
||||
import { flagUrl, initials } from '#lib/components/match/theme.js';
|
||||
import Crown from '@lucide/svelte/icons/crown';
|
||||
|
||||
interface Member {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import { gradientFor } from '$lib/format';
|
||||
import Avatar from '#lib/components/Avatar.svelte';
|
||||
import { gradientFor } from '#lib/format.js';
|
||||
import CalendarDays from '@lucide/svelte/icons/calendar-days';
|
||||
|
||||
const {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import * as Dialog from './index.js';
|
||||
import DialogPortal from './dialog-portal.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import SheetOverlay from './sheet-overlay.svelte';
|
||||
import SheetPortal from './sheet-portal.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef, type WithoutChildren } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef, type WithoutChildren } from '#lib/utils.js';
|
||||
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* One live match as the Live page and stream overlays show it: small enough
|
||||
* to send for every match on every change.
|
||||
*/
|
||||
import type { MatchView, Phase, SideColor } from '$lib/match-view';
|
||||
import type { MatchView, Phase, SideColor } from '#lib/match-view.js';
|
||||
|
||||
export interface LiveSide {
|
||||
id: string;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* every change over the websocket. Everything a viewer needs to draw the page
|
||||
* is in here; the client never refetches the page to update.
|
||||
*/
|
||||
import type { LogLine, Pause, Phase, Prompt, WinCondition } from '$lib/server/osu/procedure';
|
||||
import type { LogLine, Pause, Phase, Prompt, WinCondition } from '#lib/server/osu/procedure.js';
|
||||
|
||||
export type { LogLine, Phase, Prompt };
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface LiveScore {
|
||||
accuracy: number;
|
||||
combo: number;
|
||||
progress: number;
|
||||
/** Server receipt time, so an interrupted score feed is visible to spectators. */
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
export interface PlayerView {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { account, user, session, verification } from './db/auth.schema';
|
||||
import { playerRating } from './db/schema';
|
||||
import { and, eq, gt, inArray, sql } from 'drizzle-orm';
|
||||
import { ACTIVE_MATCH_STATES } from './match/types';
|
||||
import { newAuthIdentity } from '$lib/auth-identity';
|
||||
import { newAuthIdentity } from '#lib/auth-identity.js';
|
||||
|
||||
export const recoveryCookie = 'vash-account-recovery';
|
||||
const identifier = (token: string) =>
|
||||
@@ -12,14 +12,12 @@ const identifier = (token: string) =>
|
||||
|
||||
export async function startRecovery(userId: string) {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
await db
|
||||
.insert(verification)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
identifier: identifier(token),
|
||||
value: userId,
|
||||
expiresAt: new Date(Date.now() + 10 * 60_000)
|
||||
});
|
||||
await db.insert(verification).values({
|
||||
id: crypto.randomUUID(),
|
||||
identifier: identifier(token),
|
||||
value: userId,
|
||||
expiresAt: new Date(Date.now() + 10 * 60_000)
|
||||
});
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { APP_ENV, ORIGIN } from '$app/env/private';
|
||||
|
||||
export type AppEnv = 'dev' | 'staging' | 'prod';
|
||||
|
||||
/** APP_ENV wins; otherwise the public origin tells the three deployments apart. */
|
||||
export function appEnv(): AppEnv {
|
||||
const explicit = env.APP_ENV?.toLowerCase();
|
||||
const explicit = APP_ENV?.toLowerCase();
|
||||
if (explicit === 'dev' || explicit === 'staging' || explicit === 'prod') return explicit;
|
||||
const origin = env.ORIGIN ?? '';
|
||||
const origin = ORIGIN ?? '';
|
||||
if (origin.includes('esports-staging.')) return 'staging';
|
||||
if (
|
||||
origin.includes('esports-dev.') ||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { contactEmail } from '$lib/auth-identity';
|
||||
import { RESEND_API_KEY, EMAIL_FROM } from '$app/env/private';
|
||||
import { contactEmail } from '#lib/auth-identity.js';
|
||||
|
||||
export const emailDeliveryEnabled = !!(env.RESEND_API_KEY && env.EMAIL_FROM);
|
||||
export const emailDeliveryEnabled = !!(RESEND_API_KEY && EMAIL_FROM);
|
||||
|
||||
export async function sendAuthEmail(to: string, subject: string, url: string) {
|
||||
if (!contactEmail(to)) return;
|
||||
if (!emailDeliveryEnabled) throw new Error('Email delivery is not configured');
|
||||
const response = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
Authorization: `Bearer ${RESEND_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
|
||||
body: JSON.stringify({
|
||||
from: env.EMAIL_FROM,
|
||||
from: EMAIL_FROM,
|
||||
to: [to],
|
||||
subject,
|
||||
text: `${subject}\n\n${url}\n\nIf you did not request this, you can ignore this email.`
|
||||
|
||||
@@ -12,19 +12,17 @@ vi.mock('$app/server', () => ({ getRequestEvent: () => ({ locals: context.locals
|
||||
vi.mock('better-auth/svelte-kit', () => ({
|
||||
sveltekitCookies: () => ({ id: 'integration-cookies' })
|
||||
}));
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: {
|
||||
ORIGIN: 'http://localhost:3102',
|
||||
BETTER_AUTH_SECRET: 'test-secret-with-at-least-32-characters',
|
||||
OSU_CLIENT_ID: 'test',
|
||||
OSU_CLIENT_SECRET: 'test',
|
||||
DISCORD_CLIENT_ID: 'test',
|
||||
DISCORD_CLIENT_SECRET: 'test',
|
||||
GOOGLE_CLIENT_ID: 'test',
|
||||
GOOGLE_CLIENT_SECRET: 'test',
|
||||
RESEND_API_KEY: 'test',
|
||||
EMAIL_FROM: 'test@example.com'
|
||||
}
|
||||
vi.mock('$app/env/private', () => ({
|
||||
ORIGIN: 'http://localhost:3102',
|
||||
BETTER_AUTH_SECRET: 'test-secret-with-at-least-32-characters',
|
||||
OSU_CLIENT_ID: 'test',
|
||||
OSU_CLIENT_SECRET: 'test',
|
||||
DISCORD_CLIENT_ID: 'test',
|
||||
DISCORD_CLIENT_SECRET: 'test',
|
||||
GOOGLE_CLIENT_ID: 'test',
|
||||
GOOGLE_CLIENT_SECRET: 'test',
|
||||
RESEND_API_KEY: 'test',
|
||||
EMAIL_FROM: 'test@example.com'
|
||||
}));
|
||||
vi.mock('./auth-mail', () => ({
|
||||
emailDeliveryEnabled: true,
|
||||
@@ -32,7 +30,7 @@ vi.mock('./auth-mail', () => ({
|
||||
context.mail.push({ to, url });
|
||||
}
|
||||
}));
|
||||
vi.mock('$lib/server/db', async () => {
|
||||
vi.mock('#lib/server/db/index.js', async () => {
|
||||
const url = process.env.AUTH_TEST_DATABASE_URL;
|
||||
if (!url || !new URL(url).pathname.startsWith('/esports_auth_test'))
|
||||
throw new Error('An isolated esports_auth_test database is required');
|
||||
@@ -89,7 +87,7 @@ describe.skipIf(!enabled)('Better Auth with the actual Postgres schema', () => {
|
||||
);
|
||||
}
|
||||
beforeAll(async () => {
|
||||
const module = (await import('$lib/server/db')) as unknown as {
|
||||
const module = (await import('#lib/server/db/index.js')) as unknown as {
|
||||
db: typeof db;
|
||||
testConnection: typeof connection;
|
||||
};
|
||||
|
||||
@@ -3,21 +3,32 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||
import { sveltekitCookies } from 'better-auth/svelte-kit';
|
||||
import { genericOAuth, customSession } from 'better-auth/plugins';
|
||||
import { APIError, createAuthMiddleware } from 'better-auth/api';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
import {
|
||||
OSU_CLIENT_ID,
|
||||
OSU_CLIENT_SECRET,
|
||||
DISCORD_CLIENT_ID,
|
||||
DISCORD_CLIENT_SECRET,
|
||||
GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET,
|
||||
ORIGIN,
|
||||
BETTER_AUTH_SECRET
|
||||
} from '$app/env/private';
|
||||
|
||||
import { getRequestEvent } from '$app/server';
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { db } from './db';
|
||||
import { account, user as userTable } from './db/auth.schema';
|
||||
import { playerRating } from './db/schema';
|
||||
import { rankToElo } from './match/rating-math';
|
||||
import { contactEmail, newAuthIdentity, type ConnectionProfile } from '$lib/auth-identity';
|
||||
import { contactEmail, newAuthIdentity, type ConnectionProfile } from '#lib/auth-identity.js';
|
||||
import { emailDeliveryEnabled, sendAuthEmail } from './auth-mail';
|
||||
import { adoptProviderPicture } from './profile-picture';
|
||||
|
||||
export const configuredProviders = {
|
||||
osu: !!(env.OSU_CLIENT_ID && env.OSU_CLIENT_SECRET),
|
||||
discord: !!(env.DISCORD_CLIENT_ID && env.DISCORD_CLIENT_SECRET),
|
||||
google: !!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET)
|
||||
osu: !!(OSU_CLIENT_ID && OSU_CLIENT_SECRET),
|
||||
discord: !!(DISCORD_CLIENT_ID && DISCORD_CLIENT_SECRET),
|
||||
google: !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET)
|
||||
};
|
||||
|
||||
/** Request-local profile data survives the callback without a shared pending-user cache. */
|
||||
@@ -89,8 +100,8 @@ async function syncConnection(link: typeof account.$inferInsert) {
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: env.ORIGIN,
|
||||
secret: env.BETTER_AUTH_SECRET,
|
||||
baseURL: ORIGIN,
|
||||
secret: BETTER_AUTH_SECRET,
|
||||
database: drizzleAdapter(db, { provider: 'pg' }),
|
||||
user: {
|
||||
fields: { email: 'authEmail' },
|
||||
@@ -182,15 +193,15 @@ export const auth = betterAuth({
|
||||
? [
|
||||
{
|
||||
providerId: 'osu',
|
||||
clientId: env.OSU_CLIENT_ID!,
|
||||
clientSecret: env.OSU_CLIENT_SECRET!,
|
||||
clientId: OSU_CLIENT_ID!,
|
||||
clientSecret: OSU_CLIENT_SECRET!,
|
||||
authorizationUrl: 'https://osu.ppy.sh/oauth/authorize',
|
||||
tokenUrl: 'https://osu.ppy.sh/oauth/token',
|
||||
userInfoUrl: 'https://osu.ppy.sh/api/v2/me/osu',
|
||||
scopes: ['identify', 'public'],
|
||||
pkce: false,
|
||||
authentication: 'post' as const,
|
||||
redirectURI: `${env.ORIGIN}/api/auth/oauth2/callback/osu`,
|
||||
redirectURI: `${ORIGIN}/api/auth/oauth2/callback/osu`,
|
||||
mapProfileToUser: (profile: Record<string, any>) =>
|
||||
oauthUser('osu', String(profile.id), {
|
||||
username: profile.username,
|
||||
@@ -207,14 +218,14 @@ export const auth = betterAuth({
|
||||
? [
|
||||
{
|
||||
providerId: 'discord',
|
||||
clientId: env.DISCORD_CLIENT_ID!,
|
||||
clientSecret: env.DISCORD_CLIENT_SECRET!,
|
||||
clientId: DISCORD_CLIENT_ID!,
|
||||
clientSecret: DISCORD_CLIENT_SECRET!,
|
||||
authorizationUrl: 'https://discord.com/api/oauth2/authorize',
|
||||
tokenUrl: 'https://discord.com/api/oauth2/token',
|
||||
userInfoUrl: 'https://discord.com/api/users/@me',
|
||||
scopes: ['identify'],
|
||||
pkce: false,
|
||||
redirectURI: `${env.ORIGIN}/api/auth/oauth2/callback/discord`,
|
||||
redirectURI: `${ORIGIN}/api/auth/oauth2/callback/discord`,
|
||||
mapProfileToUser: (profile: Record<string, any>) =>
|
||||
oauthUser('discord', String(profile.id), {
|
||||
username: profile.username,
|
||||
@@ -231,13 +242,13 @@ export const auth = betterAuth({
|
||||
? [
|
||||
{
|
||||
providerId: 'google',
|
||||
clientId: env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET!,
|
||||
clientId: GOOGLE_CLIENT_ID!,
|
||||
clientSecret: GOOGLE_CLIENT_SECRET!,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
||||
scopes: ['openid', 'email', 'profile'],
|
||||
pkce: true,
|
||||
redirectURI: `${env.ORIGIN}/api/auth/oauth2/callback/google`,
|
||||
redirectURI: `${ORIGIN}/api/auth/oauth2/callback/google`,
|
||||
mapProfileToUser: (profile: Record<string, any>) =>
|
||||
oauthUser('google', String(profile.sub ?? profile.id), {
|
||||
username: profile.name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
import { COMPONENTS, SCENES, type ComponentType } from '$lib/broadcast/layout';
|
||||
import { COMPONENTS, SCENES, type ComponentType } from '#lib/broadcast/layout.js';
|
||||
|
||||
/** https URLs or paths on this site, nothing that could close a CSS url("…"). */
|
||||
const safeUrl = z
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
||||
import { and, asc, eq, gt, inArray, isNotNull, isNull, lt, sql } from 'drizzle-orm';
|
||||
import { alias } from 'drizzle-orm/pg-core';
|
||||
import { db } from '$lib/server/db';
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import {
|
||||
broadcastChannel,
|
||||
broadcastSession,
|
||||
@@ -17,25 +17,33 @@ import {
|
||||
tournament,
|
||||
tournamentMatch,
|
||||
tournamentRound
|
||||
} from '$lib/server/db/schema';
|
||||
import { hub, publish, setSnapshot } from '$lib/server/hub';
|
||||
import { log } from '$lib/server/logger';
|
||||
import { publicUrl } from '$lib/server/storage/r2';
|
||||
import { on } from '$lib/server/events';
|
||||
import { singleton } from '$lib/server/singleton';
|
||||
import { socketEndpoints } from '$lib/server/ws-attach';
|
||||
import { matchesIfRunning } from '$lib/server/match/service';
|
||||
import { predictionState } from '$lib/server/predictions';
|
||||
import type { ExternalScore } from '$lib/server/match/runtime';
|
||||
import { summarize, DECIDED, type LiveMatch } from '$lib/live';
|
||||
import type { MatchView } from '$lib/match-view';
|
||||
import type { Crowd } from '$lib/predictions';
|
||||
import { sceneFor, type Layout } from '$lib/broadcast/layout';
|
||||
import { pickMatch } from '$lib/broadcast/director';
|
||||
import type { ChannelMode, ChannelPayload, UpcomingMatch } from '$lib/broadcast/channel';
|
||||
} from '#lib/server/db/schema.js';
|
||||
import { hub, publish, setSnapshot } from '#lib/server/hub.js';
|
||||
import { log } from '#lib/server/logger.js';
|
||||
import { publicUrl } from '#lib/server/storage/r2.js';
|
||||
import { on } from '#lib/server/events.js';
|
||||
import { singleton } from '#lib/server/singleton.js';
|
||||
import { socketEndpoints } from '#lib/server/ws-attach.js';
|
||||
import { matchesIfRunning } from '#lib/server/match/service.js';
|
||||
import { predictionState } from '#lib/server/predictions.js';
|
||||
import type { ExternalScore } from '#lib/server/match/runtime.js';
|
||||
import { summarize, DECIDED, type LiveMatch } from '#lib/live.js';
|
||||
import type { MatchView } from '#lib/match-view.js';
|
||||
import type { Crowd } from '#lib/predictions.js';
|
||||
import { sceneFor, type Layout } from '#lib/broadcast/layout.js';
|
||||
import { pickMatch } from '#lib/broadcast/director.js';
|
||||
import type { ChannelMode, ChannelPayload, UpcomingMatch } from '#lib/broadcast/channel.js';
|
||||
import { parseWorkerStatus, type WorkerStatus } from '#lib/broadcast/worker.js';
|
||||
import { agentScoresSchema } from '#lib/broadcast/scores.js';
|
||||
|
||||
type ChannelRow = typeof broadcastChannel.$inferSelect;
|
||||
|
||||
interface Agent {
|
||||
send(data: string): void;
|
||||
/** What an unattended broadcast worker last said it is doing. */
|
||||
worker: WorkerStatus | null;
|
||||
}
|
||||
|
||||
interface ChannelState {
|
||||
row: ChannelRow;
|
||||
matchId: string | null;
|
||||
@@ -45,7 +53,7 @@ interface ChannelState {
|
||||
stored: MatchView | null;
|
||||
payload: ChannelPayload | null;
|
||||
publishTimer: ReturnType<typeof setTimeout> | null;
|
||||
agents: Set<{ send(data: string): void }>;
|
||||
agents: Set<Agent>;
|
||||
/** The broadcast on air right now, for chapters. */
|
||||
session: { id: string; startedAt: number } | null;
|
||||
}
|
||||
@@ -108,10 +116,14 @@ export function agentCount(channelId: string) {
|
||||
/** The match a channel shows right now, and its scene. */
|
||||
export function channelStatus(channelId: string) {
|
||||
const channel = state.channels.get(channelId);
|
||||
let worker: WorkerStatus | null = null;
|
||||
for (const agent of channel?.agents ?? [])
|
||||
if (agent.worker && (!worker || agent.worker.at > worker.at)) worker = agent.worker;
|
||||
return {
|
||||
matchId: channel?.matchId ?? null,
|
||||
scene: channel?.payload?.scene ?? 'idle',
|
||||
agents: channel?.agents.size ?? 0
|
||||
agents: channel?.agents.size ?? 0,
|
||||
worker
|
||||
};
|
||||
}
|
||||
|
||||
@@ -415,12 +427,6 @@ function tellAgents(channel: ChannelState) {
|
||||
for (const agent of channel.agents) agent.send(message);
|
||||
}
|
||||
|
||||
interface TosuFrame {
|
||||
type: 'tosu';
|
||||
clients: { osuId: number; score: number; accuracy: number; combo: number }[];
|
||||
progress: number | null;
|
||||
}
|
||||
|
||||
/** Hooks channels into match updates and registers /ws/agent. Called on every boot. */
|
||||
export function startBroadcasts() {
|
||||
on('match', 'broadcast', onMatchUpdate);
|
||||
@@ -434,16 +440,19 @@ function registerAgentSocket() {
|
||||
maxPayload: 64 * 1024,
|
||||
open(peer) {
|
||||
let channel: ChannelState | null = null;
|
||||
const agent = { send: peer.send };
|
||||
const agent: Agent = { send: peer.send, worker: null };
|
||||
return {
|
||||
message(text) {
|
||||
const message = JSON.parse(text) as {
|
||||
message(raw) {
|
||||
let message: {
|
||||
type?: string;
|
||||
channelId?: string;
|
||||
token?: string;
|
||||
clients?: TosuFrame['clients'];
|
||||
progress?: number | null;
|
||||
};
|
||||
try {
|
||||
message = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!channel) {
|
||||
const candidate =
|
||||
message.type === 'hello' && message.channelId
|
||||
@@ -470,17 +479,31 @@ function registerAgentSocket() {
|
||||
tellAgents(channel);
|
||||
return;
|
||||
}
|
||||
if (message.type === 'tosu' && channel.matchId && Array.isArray(message.clients)) {
|
||||
const scores: ExternalScore[] = message.clients
|
||||
.filter((client) => Number.isFinite(client.osuId) && client.osuId > 0)
|
||||
.map((client) => ({
|
||||
osuId: client.osuId,
|
||||
score: Math.max(0, Number(client.score) || 0),
|
||||
accuracy: Math.max(0, Math.min(100, Number(client.accuracy) || 0)),
|
||||
combo: Math.max(0, Number(client.combo) || 0),
|
||||
progress: Math.max(0, Math.min(1, Number(message.progress) || 0))
|
||||
}));
|
||||
matchesIfRunning()?.feedLive(channel.matchId, scores);
|
||||
if (message.type === 'tosu' && channel.matchId) {
|
||||
const parsed = agentScoresSchema.safeParse(message);
|
||||
if (!parsed.success) return;
|
||||
const frame = parsed.data;
|
||||
// A queued frame must not follow a director cut into a different match.
|
||||
if (frame.matchId !== undefined && frame.matchId !== channel.matchId) return;
|
||||
const scores: ExternalScore[] = frame.clients.map((client) => ({
|
||||
...client,
|
||||
progress: frame.progress ?? 0
|
||||
}));
|
||||
const accepted =
|
||||
matchesIfRunning()?.feedLive(channel.matchId, scores, frame.beatmapId) ?? 0;
|
||||
peer.send(JSON.stringify({ type: 'scores', matchId: channel.matchId, accepted }));
|
||||
} else if (message.type === 'status') {
|
||||
const before = agent.worker?.state;
|
||||
agent.worker = parseWorkerStatus(message as Record<string, unknown>);
|
||||
if (agent.worker.state !== before)
|
||||
log.broadcast.info(
|
||||
{
|
||||
channel: channel.row.name,
|
||||
state: agent.worker.state,
|
||||
detail: agent.worker.detail
|
||||
},
|
||||
'broadcast worker state'
|
||||
);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
uniqueIndex
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { jsonb } from './jsonb';
|
||||
import type { ConnectionProfile } from '$lib/auth-identity';
|
||||
import type { ConnectionProfile } from '#lib/auth-identity.js';
|
||||
|
||||
export const user = pgTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user