diff --git a/sites/scouting/.gitignore b/sites/scouting/.gitignore new file mode 100644 index 0000000..66fe16c --- /dev/null +++ b/sites/scouting/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.local +.claude/settings.local.json diff --git a/sites/scouting/.gitlab-ci.yml b/sites/scouting/.gitlab-ci.yml new file mode 100644 index 0000000..1aaf4a1 --- /dev/null +++ b/sites/scouting/.gitlab-ci.yml @@ -0,0 +1,29 @@ +stages: + - build + - deploy + +build: + stage: build + script: + - npm ci + - npm run build + artifacts: + paths: + - dist/ + expire_in: 1 hour + only: + - main + except: + - schedules + +deploy: + stage: deploy + script: + - rsync -avz --delete dist/ /var/www/domains/gateway-gamers.net/scouting/ + only: + - main + except: + - schedules + environment: + name: production + url: https://scouting.gateway-gamers.net diff --git a/sites/scouting/index.html b/sites/scouting/index.html new file mode 100644 index 0000000..506dc47 --- /dev/null +++ b/sites/scouting/index.html @@ -0,0 +1,172 @@ + + + + + + Gateway Gamers — Scouting + + + + + + +
+ + +
+

BCP Roster Scout

+

Paste a Best Coast Pairings event link for instant roster stats

+
+ + +
+ + +
+ + +
+ + Have a BCP subscription? Add your auth token to backfill missing faction/disposition data + +
+ + + +
+

+ Stored only in your browser (localStorage) — sent only to BCP's own API, never committed or deployed. + Tokens expire after about an hour, so you may need to paste a fresh one occasionally. +

+

+
+ + + + + + + + +
+ Paste a BCP event link above to see roster stats — faction spread, dispositions, checked-in rate, and team breakdowns. +
+ + + + +
+ +

+ Data from Best Coast Pairings · roster-only (this tool doesn't yet read pairings/placings) +

+ +
+ + + + + + diff --git a/sites/scouting/package-lock.json b/sites/scouting/package-lock.json new file mode 100644 index 0000000..2b61f44 --- /dev/null +++ b/sites/scouting/package-lock.json @@ -0,0 +1,1868 @@ +{ + "name": "scouting-gateway-gamers", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scouting-gateway-gamers", + "version": "0.0.1", + "devDependencies": { + "@tailwindcss/postcss": "^4.0.0", + "daisyui": "^5.0.0", + "tailwindcss": "^4.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/daisyui": { + "version": "5.7.16", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.16.tgz", + "integrity": "sha512-V9CJxvrWIXTDuP/0tpijjWaKaKJNYo2IrULJfTWs3/DjW9rPqswk1n8NCLQRLrTutmlG2Ech7nkJDpCnF+6Dzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/sites/scouting/package.json b/sites/scouting/package.json new file mode 100644 index 0000000..f0d0720 --- /dev/null +++ b/sites/scouting/package.json @@ -0,0 +1,17 @@ +{ + "name": "scouting-gateway-gamers", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.0.0", + "daisyui": "^5.0.0", + "tailwindcss": "^4.0.0", + "vite": "^6.0.0" + } +} diff --git a/sites/scouting/postcss.config.js b/sites/scouting/postcss.config.js new file mode 100644 index 0000000..1d5a3b2 --- /dev/null +++ b/sites/scouting/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {} + } +} diff --git a/sites/scouting/public/footer.html b/sites/scouting/public/footer.html new file mode 100644 index 0000000..35977fb --- /dev/null +++ b/sites/scouting/public/footer.html @@ -0,0 +1,3 @@ + diff --git a/sites/scouting/public/header.html b/sites/scouting/public/header.html new file mode 100644 index 0000000..c4923b7 --- /dev/null +++ b/sites/scouting/public/header.html @@ -0,0 +1,57 @@ +
+ + +
+ + + + + +
+ + +
+ + Gateway Gamers + +
+ +
+ + + + +
diff --git a/sites/scouting/public/images/logo.png b/sites/scouting/public/images/logo.png new file mode 100644 index 0000000..0ea6f7e Binary files /dev/null and b/sites/scouting/public/images/logo.png differ diff --git a/sites/scouting/public/images/photo1.jpg b/sites/scouting/public/images/photo1.jpg new file mode 100644 index 0000000..1bc8791 Binary files /dev/null and b/sites/scouting/public/images/photo1.jpg differ diff --git a/sites/scouting/public/images/photo2.jpg b/sites/scouting/public/images/photo2.jpg new file mode 100644 index 0000000..6484be1 Binary files /dev/null and b/sites/scouting/public/images/photo2.jpg differ diff --git a/sites/scouting/public/images/photo3.jpg b/sites/scouting/public/images/photo3.jpg new file mode 100644 index 0000000..152560f Binary files /dev/null and b/sites/scouting/public/images/photo3.jpg differ diff --git a/sites/scouting/public/images/photo4.jpg b/sites/scouting/public/images/photo4.jpg new file mode 100644 index 0000000..3bf4a4f Binary files /dev/null and b/sites/scouting/public/images/photo4.jpg differ diff --git a/sites/scouting/public/images/photo5.jpg b/sites/scouting/public/images/photo5.jpg new file mode 100644 index 0000000..5c4807a Binary files /dev/null and b/sites/scouting/public/images/photo5.jpg differ diff --git a/sites/scouting/scouting.gateway-gamers.net.conf b/sites/scouting/scouting.gateway-gamers.net.conf new file mode 100644 index 0000000..924daf5 --- /dev/null +++ b/sites/scouting/scouting.gateway-gamers.net.conf @@ -0,0 +1,41 @@ +server { + listen 80; + listen [::]:80; + server_name scouting.gateway-gamers.net; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name scouting.gateway-gamers.net; + + root /var/www/domains/gateway-gamers.net/scouting; + index index.html; + + # SSL — managed by Certbot + # ssl_certificate /etc/letsencrypt/live/scouting.gateway-gamers.net/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/scouting.gateway-gamers.net/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Hide nginx version + server_tokens off; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + # style-src needs 'unsafe-inline': the app sets bar-chart widths via + # inline style="width:X%" through innerHTML, which CSP's style-src + # blocks without it (script-src stays locked down — this only affects + # CSS, and nothing here renders untrusted external content into a + # style attribute). + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://newprod-api.bestcoastpairings.com; img-src 'self' data:;" always; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/sites/scouting/src/main.js b/sites/scouting/src/main.js new file mode 100644 index 0000000..2c861bd --- /dev/null +++ b/sites/scouting/src/main.js @@ -0,0 +1,822 @@ +/* ---- Shared: header / footer ---- */ +function loadHTML(file, elementId, callback) { + fetch(file) + .then(r => r.text()) + .then(html => { + document.getElementById(elementId).innerHTML = html; + if (callback) callback(); + }) + .catch(err => console.error('Error loading:', file, err)); +} + +function highlightActiveNav() { + const current = location.pathname.split('/').pop() || 'index.html'; + document.querySelectorAll('#header a[href]').forEach(link => { + if (link.getAttribute('href') === `/${current}`) { + link.classList.add('active'); + } + }); +} + +function startCarousel() { + const slides = document.querySelectorAll('#hero-carousel .carousel-slide'); + if (!slides.length) return; + let current = 0; + slides[current].style.opacity = '1'; + setInterval(() => { + slides[current].style.opacity = '0'; + current = (current + 1) % slides.length; + slides[current].style.opacity = '1'; + }, 5000); +} + +loadHTML('/header.html', 'header', () => { + highlightActiveNav(); + startCarousel(); +}); +loadHTML('/footer.html', 'footer'); + +/* ---- BCP API ---- */ +const API = "https://newprod-api.bestcoastpairings.com/v1"; +const HEADERS = { "client-id": "web-app", "env": "bcp", "accept": "application/json" }; +const BCP_ORIGIN = "https://www.bestcoastpairings.com"; + +async function fetchJSON(url, extraHeaders) { + const r = await fetch(url, { headers: { ...HEADERS, ...extraHeaders }, credentials: 'omit' }); + if (!r.ok) throw new Error(`http/${r.status}`); + return r.json(); +} +const fetchEvent = id => fetchJSON(`${API}/events/${id}`); +const fetchRoster = async id => (await fetchJSON(`${API}/events/${id}/players`)).active || []; +const fetchTeamplayers = async id => (await fetchJSON(`${API}/events/${id}/teamplayers`)).active || []; + +// Requires the viewer's own BCP auth token (subscriber-only endpoint). +const fetchArmyList = (listId, token) => fetchJSON(`${API}/armylists/${listId}`, { authorization: `Bearer ${token}` }); + +// "ITC Rank" = a player's placing in BCP league "2026 Warhammer 40,000 ITC" +// (leagueId BYaaUfKum7z0, US region) — the national ITC leaderboard, same +// source the rankings site uses for its own ITC Rank column. +const ITC_LEAGUE_ID = "BYaaUfKum7z0"; +const ITC_REGION_ID = "VgQKgqmTPU"; + +// Cursor-paginated league placings (BCP returns the same cursor when there +// are no more results — stop when it loops). +async function* streamPlacings(leagueId, regionId) { + let base = `${API}/placings?placingsType=player&leagueId=${leagueId}&sortAscending=false&limit=1000`; + if (regionId) base += `®ionId=${regionId}`; + + let nextKey = null; + let prevNextKey = undefined; + + do { + const url = nextKey ? `${base}&nextKey=${encodeURIComponent(nextKey)}` : base; + const r = await fetch(url, { headers: HEADERS, credentials: "omit" }); + if (!r.ok) throw new Error("placings/" + r.status); + const p = await r.json(); + const page = Array.isArray(p) ? p : (p.data || p.placings || []); + + yield page; + + const rawKey = p.nextKey; + nextKey = !rawKey ? null + : typeof rawKey === "string" ? rawKey + : btoa(JSON.stringify(rawKey)); + + if (nextKey === prevNextKey) break; + prevNextKey = nextKey; + } while (nextKey); +} + +// Career win/loss/tie record, summed across every event in a player's BCP +// placings history (same source and shape the 40k-rankings site uses for +// its own win% column). +async function fetchCareerRecord(userId) { + const r = await fetch( + `${API}/placings?placingsType=player&userId=${userId}&limit=1000`, + { headers: HEADERS, credentials: "omit" } + ); + if (!r.ok) return { w: 0, l: 0, t: 0 }; + const p = await r.json(); + const items = Array.isArray(p) ? p : (p.data || []); + let w = 0, l = 0, t = 0; + for (const item of items) { + const v = item.value || item; + w += +(v.wins || 0); + l += +(v.losses || 0); + t += +(v.ties || 0); + } + return { w, l, t }; +} + +async function loadCareerRecords(userIds) { + const records = new Map(); + await Promise.allSettled([...userIds].map(async uid => { + try { records.set(uid, await fetchCareerRecord(uid)); } + catch { records.set(uid, { w: 0, l: 0, t: 0 }); } + })); + return records; +} + +async function loadItcRanks(userIds) { + const ranks = new Map(); + if (!userIds.size) return ranks; + try { + for await (const page of streamPlacings(ITC_LEAGUE_ID, ITC_REGION_ID)) { + for (const rec of page) { + const uid = rec.userId || rec.user?.id; + if (uid && userIds.has(uid) && rec.placing != null && !ranks.has(uid)) { + ranks.set(uid, rec.placing); + } + } + if (ranks.size >= userIds.size) break; // found everyone we need + } + } catch (e) { + console.warn("ITC rank load failed:", e); + } + return ranks; +} + +/* ---- BCP auth token (subscriber-only, for the list backfill below) ---- + Stored only in this browser's localStorage. Sent only in requests this + browser makes directly to BCP's own API — never to any other server, + never committed, never part of the deployed bundle. */ +const TOKEN_KEY = "bcp_auth_token"; +const getToken = () => { try { return localStorage.getItem(TOKEN_KEY) || ""; } catch { return ""; } }; +const setToken = t => { try { t ? localStorage.setItem(TOKEN_KEY, t) : localStorage.removeItem(TOKEN_KEY); } catch {} }; + +// This event tracks the five ITC-standard mission dispositions. Hand-typed +// sources (a "Dispositions Used:" line in a submitted list, BCP's own free +// text) routinely have small typos (e.g. "Priority Assests"), so every +// disposition value is fuzzy-matched against this fixed list before being +// counted or displayed, keeping typo'd variants from forking off as their +// own duplicate bar. +const CANONICAL_DISPOSITIONS = ["Purge the Foe", "Take and Hold", "Priority Assets", "Disruption", "Reconnaissance"]; + +function levenshtein(a, b) { + const m = a.length, n = b.length; + if (!m) return n; + if (!n) return m; + let prev = Array.from({ length: n + 1 }, (_, i) => i); + for (let i = 1; i <= m; i++) { + const cur = [i]; + for (let j = 1; j <= n; j++) { + cur[j] = a[i - 1] === b[j - 1] ? prev[j - 1] : 1 + Math.min(prev[j - 1], prev[j], cur[j - 1]); + } + prev = cur; + } + return prev[n]; +} + +const normalizeDispositionText = s => String(s || "").trim().toLowerCase().replace(/[^a-z ]/g, "").replace(/\s+/g, " "); + +// Filler-word-stripped form of each canonical name, so abbreviations that +// drop "and"/"the" (e.g. "Take Hold") still line up: "Take and Hold" -> "take hold". +const dispositionCore = canonical => normalizeDispositionText(canonical).replace(/\b(and|the)\b/g, "").replace(/\s+/g, " ").trim(); + +// Nearest canonical disposition for a single (already-normalized) segment, or +// null if nothing is close enough to be confident it's a typo/abbreviation +// rather than unrelated text. Two ways to match: +// - Prefix: shorthand like "Recon" or "Disrupt" that's a truncation, not a +// typo, so Levenshtein distance alone would score it as far away. Gated +// to 3+ chars so short unrelated words can't collide (the five names' +// first three letters — pur/tak/pri/dis/rec — are all distinct). +// - Levenshtein: everything else, e.g. "Priority Assests". Threshold scales +// with name length (~25%) so a short unrelated line doesn't accidentally +// match "Disruption". +function matchDispositionSegment(norm) { + if (!norm) return null; + let best = null, bestDist = Infinity; + for (const canonical of CANONICAL_DISPOSITIONS) { + const full = canonical.toLowerCase(); + const core = dispositionCore(canonical); + if (norm.length >= 3 && (full.startsWith(norm) || core.startsWith(norm))) return canonical; + const dist = Math.min(levenshtein(norm, full), levenshtein(norm, core)); + if (dist < bestDist) { bestDist = dist; best = canonical; } + } + const threshold = Math.max(1, Math.round(best.length * 0.25)); + return bestDist <= threshold ? best : null; +} + +// Some sources list two dispositions together, e.g. "Purge the Foe +// (Reconnaissance)" (a team template noting a secondary/alternate) or +// "Take and Hold / Priority Assets". Split on the separators that join them +// and match segment-by-segment left to right, so whichever is listed first +// wins rather than whichever happens to score closer across the whole string. +function closestDisposition(name) { + const raw = String(name || ""); + const segments = raw.split(/[()\/|,;]+|\s+vs\.?\s+/i).map(s => s.trim()).filter(Boolean); + for (const seg of segments.length ? segments : [raw]) { + const match = matchDispositionSegment(normalizeDispositionText(seg)); + if (match) return match; + } + return null; +} + +const correctDisposition = name => closestDisposition(name) || name; + +// The vocabulary of disposition names is whatever this event's own roster +// already shows (e.g. "Take and Hold", "Priority Assets", …) — deriving it +// from the event rather than hardcoding keeps this working for other game +// systems/mission packs with different disposition names. Keyed by lowercase +// so matches are case-insensitive (a list's free text may not match the +// roster's casing), with the value holding the roster's canonical casing so +// backfilled entries merge into the same bar instead of forking a duplicate. +function knownDispositions(roster) { + const map = new Map(); + for (const r of roster) { + const name = r.subFaction?.name; + if (name && !map.has(name.toLowerCase())) map.set(name.toLowerCase(), name); + } + return map; +} + +// A submitted list's faction is a clean field (`army.name`). Its disposition +// isn't — `warhammer.detachment` is the in-game GW detachment name (e.g. +// "Wrath of the Rock"), not BCP's ITC disposition bucket. The disposition +// only appears as a plain line in the exported list text, so we scan for a +// line that exactly matches one of this event's known disposition names. +function extractFactionAndDisposition(armyList, knownDisp) { + const text = String(armyList.armyListText || ""); + + // Clean structured field — present when BCP resolved the list to a + // canonical army entity (not every list gets one; some submission + // templates skip it entirely, e.g. narrative/story-post formats). + let faction = armyList.army?.name || armyList.player?.army?.name || null; + // Fallback: some team templates include an explicit "Factions Used: X" + // label line in the free text even when the structured field is absent. + if (!faction) { + const m = text.match(/^Factions?\s+Used:\s*(.+)$/im); + if (m) faction = m[1].trim(); + } + // Fallback: BCP's own auto-generated list block (the second half of the + // text, distinct from whatever custom template a team prepends) always + // has the faction name on its own line immediately before the line + // naming the detachment, e.g. "…\nEmperor's Children\nCarnival of Excess + // and Frenzied Host (3 Detachment Points)\n…". This anchor holds across + // every template we've seen, unlike any specific label text. + if (!faction) { + const lines = text.split("\n").map(l => l.trim()); + const detachmentLineIdx = lines.findIndex(l => /Detachment Points\)/i.test(l)); + for (let i = detachmentLineIdx - 1; i >= 0; i--) { + if (lines[i]) { faction = lines[i]; break; } + } + } + + // Disposition has no clean structured field at all (BCP's `warhammer` + // object only carries the in-game GW detachment name, e.g. "Wrath of the + // Rock" — not the ITC disposition bucket). Try the explicit label first, + // then fall back to scanning for a standalone line matching this event's + // known disposition vocabulary (BCP's generated list block always + // includes one, right after the detachment line). + let disposition = null; + const dm = text.match(/^Dispositions?\s+Used:\s*(.+)$/im); + if (dm) { + const captured = dm[1].trim(); + disposition = knownDisp.get(captured.toLowerCase()) || closestDisposition(captured) || captured; + } + if (!disposition) { + for (const rawLine of text.split("\n")) { + const line = rawLine.trim(); + if (!line) continue; + const canonical = knownDisp.get(line.toLowerCase()) || closestDisposition(line); + if (canonical) { disposition = canonical; break; } + } + } + + return { faction, disposition }; +} + +async function backfillFromLists(roster, token) { + const knownDisp = knownDispositions(roster); + const targets = roster.filter(r => r.listId && (!r.faction?.name || !r.subFaction?.name)); + const backfill = new Map(); // roster row id -> { faction, disposition } + if (!targets.length) return { backfill, tokenInvalid: false, attempted: 0 }; + + // Sequential, not parallel: this is a subscriber-gated endpoint, and firing + // many requests at once risks rate-limiting some of them silently. Each + // failure/miss is logged so it's diagnosable from the browser console. + let tokenInvalid = false; + for (const r of targets) { + if (tokenInvalid) break; + const label = fullName(r.user || {}) || r.id; + try { + const list = await fetchArmyList(r.listId, token); + const { faction, disposition } = extractFactionAndDisposition(list, knownDisp); + if (faction || disposition) { + backfill.set(r.id, { faction, disposition }); + } else { + console.warn(`[backfill] ${label}: list fetched but no faction/disposition found in it`, list); + } + } catch (e) { + const msg = String(e.message || e); + if (msg.includes("401") || msg.includes("403")) { + tokenInvalid = true; + console.warn(`[backfill] ${label}: auth failed (${msg}) — stopping remaining lookups`); + } else { + console.warn(`[backfill] ${label}: fetch failed (${msg})`); + } + } + } + return { backfill, tokenInvalid, attempted: targets.length }; +} + +// Non-destructive: only fills fields that were actually missing, and tags +// what it touched so the UI can mark it as inferred. +function applyBackfill(roster, backfill) { + if (!backfill || !backfill.size) return roster; + return roster.map(r => { + const b = backfill.get(r.id); + if (!b) return r; + const patch = {}; + if (b.faction && !r.faction?.name) { patch.faction = { name: b.faction }; patch._backfilledFaction = true; } + if (b.disposition && !r.subFaction?.name) { patch.subFaction = { name: b.disposition }; patch._backfilledDisposition = true; } + return Object.keys(patch).length ? { ...r, ...patch } : r; + }); +} + +/* ---- Helpers ---- */ +const $ = id => document.getElementById(id); +const esc = s => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); +const fullName = (u = {}) => [u.firstName, u.lastName].filter(Boolean).join(" ").trim(); +const pct = (n, d) => d ? Math.round(n / d * 100) + "%" : "—"; +const fmtWpct = (w, l, t) => { const g = (+w || 0) + (+l || 0) + (+t || 0); return g ? ((+w || 0) / g * 100).toFixed(1) + "%" : "—"; }; + +function parseEventId(input) { + const s = String(input || "").trim(); + if (!s) return null; + const m = s.match(/\/event\/([A-Za-z0-9]+)/); + if (m) return m[1]; + const bare = s.split("?")[0].split("#")[0].trim(); + return /^[A-Za-z0-9]+$/.test(bare) ? bare : null; +} + +function fmtDateRange(ev) { + const start = ev.eventDate ? new Date(ev.eventDate) : null; + const end = ev.eventEndDate ? new Date(ev.eventEndDate) : null; + const opts = { month: "short", day: "numeric", year: "numeric" }; + if (!start) return ""; + if (end && end.toDateString() !== start.toDateString()) { + return `${start.toLocaleDateString(undefined, opts)} – ${end.toLocaleDateString(undefined, opts)}`; + } + return start.toLocaleDateString(undefined, opts); +} + +/* ---- Stat computation (pure functions over the roster) ---- */ +function computeCounts(roster, keyFn) { + const m = new Map(); + for (const r of roster) { + const key = keyFn(r) || "Unknown"; + m.set(key, (m.get(key) || 0) + 1); + } + return [...m.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count); +} + +const computeFactionCounts = roster => computeCounts(roster, r => r.faction?.name); +// Dispositions are counted across all factions together (e.g. "Take and Hold" +// combines every faction running that disposition), not split per-faction. +const computeDispositionCounts = roster => computeCounts(roster, r => r.subFaction?.name); + +// Per-faction disposition breakdown, for the drill-down under each faction bar. +function computeDispositionsByFaction(roster) { + const byFaction = new Map(); + for (const r of roster) { + const faction = r.faction?.name || "Unknown"; + if (!byFaction.has(faction)) byFaction.set(faction, []); + byFaction.get(faction).push(r); + } + const result = new Map(); + for (const [faction, players] of byFaction) { + result.set(faction, computeDispositionCounts(players)); + } + return result; +} + +// Players link to their event-team via teamPlayerId → teamplayers[].id +// (the free-text team name embedded on the player record can be a typo'd +// variant a player typed themselves, so it isn't a safe grouping key). +function computeTeamRollups(roster, teamplayers) { + const byTeamPlayerId = new Map(); + for (const r of roster) { + const key = r.teamPlayerId; + if (!key) continue; + if (!byTeamPlayerId.has(key)) byTeamPlayerId.set(key, []); + byTeamPlayerId.get(key).push(r); + } + return teamplayers.map(tp => { + const members = byTeamPlayerId.get(tp.id) || []; + const withList = members.filter(m => m.listUrl).length; + return { + id: tp.id, + name: tp.name || "Unnamed Team", + captain: fullName(tp.captain || {}) || "—", + size: members.length, + withList, + members, + }; + }).sort((a, b) => a.name.localeCompare(b.name)); +} + +// Top/average/bottom ITC rank for a team, computed only from members who +// have a resolved rank (lower placing number = better). +function computeItcStats(members, itcRanks) { + const ranks = members + .map(m => itcRanks.get(m.userId || m.user?.id)) + .filter(r => r != null); + if (!ranks.length) return null; + return { + top: Math.min(...ranks), + bottom: Math.max(...ranks), + avg: Math.round(ranks.reduce((s, r) => s + r, 0) / ranks.length), + }; +} + +function computeInsights(roster, teams, isTeamEvent) { + const insights = []; + const total = roster.length; + if (!total) return insights; + + const factionCounts = computeFactionCounts(roster); + if (factionCounts.length) { + const top = factionCounts[0]; + insights.push(`${esc(top.name)} is the most popular faction — ${top.count} of ${total} players (${pct(top.count, total)}).`); + } + + const withList = roster.filter(r => r.listUrl).length; + insights.push(`${withList} of ${total} players (${pct(withList, total)}) have a submitted list.`); + + const checkedIn = roster.filter(r => r.checkedIn).length; + if (checkedIn > 0) { + insights.push(`${checkedIn} of ${total} players (${pct(checkedIn, total)}) are checked in.`); + } + + if (isTeamEvent && teams.length) { + const withPlayers = teams.filter(t => t.size > 0); + const fullList = withPlayers.filter(t => t.withList === t.size); + const noList = withPlayers.filter(t => t.withList === 0); + if (fullList.length) { + insights.push(`${fullList.length} team${fullList.length === 1 ? "" : "s"} ${fullList.length === 1 ? "has" : "have"} every player's list submitted.`); + } + if (noList.length) { + insights.push(`${noList.length} team${noList.length === 1 ? "" : "s"} ${noList.length === 1 ? "hasn't" : "haven't"} submitted any lists yet.`); + } + } + + return insights.slice(0, 4); +} + +/* ---- Rendering ---- */ +function renderBars(container, items, limit) { + const total = items.reduce((s, it) => s + it.count, 0); + const shown = limit ? items.slice(0, limit) : items; + if (!shown.length) { + container.innerHTML = `

No data.

`; + return; + } + container.innerHTML = shown.map(it => { + const w = total ? Math.max(it.count / total * 100, 2) : 0; + return ` +
+
${esc(it.name)}
+
+
+
+
${it.count} · ${pct(it.count, total)}
+
`; + }).join(""); +} + +// Faction bars, each expandable to show that faction's own disposition breakdown. +function renderFactionBreakdown(container, factionCounts, dispositionsByFaction) { + const total = factionCounts.reduce((s, it) => s + it.count, 0); + if (!factionCounts.length) { + container.innerHTML = `

No data.

`; + return; + } + container.innerHTML = factionCounts.map(it => { + const w = total ? Math.max(it.count / total * 100, 2) : 0; + const subItems = dispositionsByFaction.get(it.name) || []; + const subTotal = subItems.reduce((s, x) => s + x.count, 0); + const subRows = subItems.map(sub => { + const sw = subTotal ? Math.max(sub.count / subTotal * 100, 2) : 0; + return ` +
+
${esc(sub.name)}
+
+
+
+
${sub.count} · ${pct(sub.count, subTotal)}
+
`; + }).join(""); + + return ` +
+ +
+
+
${esc(it.name)}
+
+
+
+
${it.count} · ${pct(it.count, total)}
+
+
+
+
${subRows}
+
+
`; + }).join(""); +} + +function playerRowHTML(r, itcRanks, itcLoading, careerRecords, careerLoading) { + const name = fullName(r.user || {}) || r.user?.nickname || "Unknown"; + const listCell = r.listUrl + ? `View` + : ``; + const uid = r.userId || r.user?.id; + const rank = itcRanks.get(uid); + const rankLabel = itcLoading ? "…" : (rank != null ? `#${rank}` : "—"); + const record = careerRecords.get(uid); + const wpctLabel = careerLoading ? "…" : fmtWpct(record?.w, record?.l, record?.t); + const wpctTitle = record ? ` title="${record.w || 0}-${record.l || 0}-${record.t || 0} career"` : ""; + const inferred = text => `${text}`; + const factionCell = r._backfilledFaction ? inferred(esc(r.faction?.name || "—")) : esc(r.faction?.name || "—"); + const dispositionCell = r._backfilledDisposition ? inferred(esc(r.subFaction?.name || "—")) : esc(r.subFaction?.name || "—"); + return ` + + ${esc(name)} + ${factionCell} + ${dispositionCell} + ${rankLabel} + ${wpctLabel} + ${listCell} + `; +} + +function memberMatches(m, teamName, q) { + const hay = [fullName(m.user || {}), m.user?.nickname, teamName, m.faction?.name, m.subFaction?.name] + .filter(Boolean).join(" ").toLowerCase(); + return hay.includes(q); +} + +function renderTeamList(teams, itcRanks, itcLoading, careerRecords, careerLoading, filterText) { + const q = (filterText || "").trim().toLowerCase(); + + const blocks = teams.map(t => { + const members = t.members.slice().sort((a, b) => fullName(a.user || {}).localeCompare(fullName(b.user || {}))); + // Matching the team name pulls in every member, since each member's + // haystack already includes the team name. + const shown = q ? members.filter(m => memberMatches(m, t.name, q)) : members; + if (q && !shown.length) return ""; + + const stats = computeItcStats(t.members, itcRanks); + const fmtRank = v => (itcLoading ? "…" : v == null ? "—" : `#${v}`); + + return ` +
+ +
+
+
+
${esc(t.name)}
+
Captain: ${esc(t.captain)} · ${t.size} players
+
+
+
Top
${fmtRank(stats?.top)}
+
Avg
${fmtRank(stats?.avg)}
+
Bottom
${fmtRank(stats?.bottom)}
+
+
+
+
+
+ + + + + + + + + + + + ${shown.map(m => playerRowHTML(m, itcRanks, itcLoading, careerRecords, careerLoading)).join("")} +
PlayerFactionITC RankCareer W%List
+
+
+
`; + }).filter(Boolean); + + $("team-list").innerHTML = blocks.join("") || + `

No teams match${q ? ` "${esc(filterText)}"` : ""}.

`; +} + +function renderPlayers(roster, itcRanks, itcLoading, careerRecords, careerLoading, filterText) { + const q = (filterText || "").trim().toLowerCase(); + const rows = roster.filter(r => { + if (!q) return true; + const hay = [fullName(r.user || {}), r.user?.nickname, r.faction?.name, r.subFaction?.name] + .filter(Boolean).join(" ").toLowerCase(); + return hay.includes(q); + }).sort((a, b) => fullName(a.user || {}).localeCompare(fullName(b.user || {}))); + + $("player-rows").innerHTML = rows.map(r => playerRowHTML(r, itcRanks, itcLoading, careerRecords, careerLoading)).join(""); + + $("player-count").textContent = q + ? `Showing ${rows.length} of ${roster.length} players` + : `${roster.length} players`; +} + +/* ---- Orchestration ---- */ +let state = null; // { event, roster, teamplayers, isTeamEvent, itcRanks, itcLoading, backfill, backfillStatus } +let dispositionExpanded = false; + +function showError(msg) { + $("loading").classList.add("hidden"); + $("results").classList.add("hidden"); + $("empty").classList.remove("hidden"); + const b = $("banner"); + b.className = "alert alert-error mb-6"; + b.innerHTML = esc(msg); +} + +function clearBanner() { + const b = $("banner"); + b.className = "alert mb-6 hidden"; + b.innerHTML = ""; +} + +function render() { + if (!state) return; + const { event, teamplayers, isTeamEvent, itcRanks, itcLoading, careerRecords, careerLoading, backfill, backfillStatus } = state; + const roster = applyBackfill(state.roster, backfill).map(r => + r.subFaction?.name + ? { ...r, subFaction: { ...r.subFaction, name: correctDisposition(r.subFaction.name) } } + : r + ); + const teams = isTeamEvent ? computeTeamRollups(roster, teamplayers) : []; + + const tokenStatusEl = $("bcp-token-status"); + if (tokenStatusEl) { + let text = ""; + if (backfillStatus === "loading") text = "Backfilling missing data from submitted lists…"; + else if (backfillStatus === "none") text = "Nothing to backfill for this event."; + else if (backfillStatus === "done" || backfillStatus === "invalid") { + text = `Backfilled ${backfill.size} of ${state.backfillAttempted} missing entries.`; + if (backfillStatus === "invalid") text += " Token invalid or expired — stopped early."; + else if (backfill.size < state.backfillAttempted) text += " Some lists had no extractable faction/disposition — see browser console."; + } + tokenStatusEl.textContent = text; + } + + $("ev-name").textContent = event.name || "Untitled Event"; + const parts = [fmtDateRange(event), [event.locationName, event.city].filter(Boolean).join(", ")] + .filter(Boolean); + if (event.numberOfRounds) parts.push(`${event.numberOfRounds} rounds`); + $("ev-sub").textContent = parts.join(" · ") || "—"; + const statusEl = $("ev-status"); + if (event.ended) { statusEl.textContent = "Ended"; statusEl.className = "badge badge-lg badge-neutral"; } + else if (event.active) { statusEl.textContent = "In Progress"; statusEl.className = "badge badge-lg badge-success"; } + else { statusEl.textContent = "Upcoming"; statusEl.className = "badge badge-lg badge-warning"; } + + $("s-players").textContent = roster.length; + $("s-teams-wrap").classList.toggle("hidden", !isTeamEvent); + $("s-teams").textContent = teams.length; + $("s-checkin").textContent = pct(roster.filter(r => r.checkedIn).length, roster.length); + $("s-lists").textContent = pct(roster.filter(r => r.listUrl).length, roster.length); + + $("insights").innerHTML = computeInsights(roster, teams, isTeamEvent) + .map(html => `
  • ${html}
  • `) + .join("") || `
  • Not enough data yet.
  • `; + + renderFactionBreakdown($("faction-bars"), computeFactionCounts(roster), computeDispositionsByFaction(roster)); + + const dispositions = computeDispositionCounts(roster); + renderBars($("disposition-bars"), dispositions, dispositionExpanded ? null : 10); + $("disposition-toggle").classList.toggle("hidden", dispositions.length <= 10); + $("disposition-toggle").textContent = dispositionExpanded ? "Show top 10" : "Show all"; + + $("team-list").classList.toggle("hidden", !isTeamEvent); + $("player-table-wrap").classList.toggle("hidden", isTeamEvent); + + const filterText = $("player-filter").value; + if (isTeamEvent) { + renderTeamList(teams, itcRanks, itcLoading, careerRecords, careerLoading, filterText); + const q = filterText.trim(); + $("player-count").textContent = q + ? `Filtering ${teams.length} teams for "${q}"` + : `${roster.length} players across ${teams.length} teams`; + } else { + renderPlayers(roster, itcRanks, itcLoading, careerRecords, careerLoading, filterText); + } + + $("loading").classList.add("hidden"); + $("empty").classList.add("hidden"); + $("results").classList.remove("hidden"); +} + +function startBackfill(capturedEvent) { + const token = getToken(); + if (!token || !state) return; + state.backfillStatus = "loading"; + render(); + backfillFromLists(state.roster, token).then(({ backfill, tokenInvalid, attempted }) => { + if (!state || state.event !== capturedEvent) return; // a newer event loaded meanwhile + state.backfill = backfill; + state.backfillAttempted = attempted; + state.backfillStatus = tokenInvalid ? "invalid" : (attempted === 0 ? "none" : "done"); + render(); + }); +} + +async function loadEvent(rawInput) { + const id = parseEventId(rawInput); + if (!id) { + showError("That doesn't look like a valid BCP event link or ID. Paste something like https://www.bestcoastpairings.com/event/oLpMmIbQMOF1"); + return; + } + + clearBanner(); + $("results").classList.add("hidden"); + $("empty").classList.add("hidden"); + $("loading").classList.remove("hidden"); + + const url = new URL(location.href); + url.searchParams.set("event", id); + history.replaceState(null, "", url); + + try { + const [event, roster, teamplayers] = await Promise.all([ + fetchEvent(id), + fetchRoster(id), + fetchTeamplayers(id).catch(() => []), // individual events have no teamplayers + ]); + + const isTeamEvent = teamplayers.length > 0; + + state = { + event, roster, teamplayers, isTeamEvent, + itcRanks: new Map(), itcLoading: true, + careerRecords: new Map(), careerLoading: true, + backfill: new Map(), backfillStatus: "idle", backfillAttempted: 0, + }; + render(); + + const userIds = new Set(roster.map(r => r.userId || r.user?.id).filter(Boolean)); + loadItcRanks(userIds).then(ranks => { + if (state && state.event === event) { + state.itcRanks = ranks; + state.itcLoading = false; + render(); + } + }); + + loadCareerRecords(userIds).then(records => { + if (state && state.event === event) { + state.careerRecords = records; + state.careerLoading = false; + render(); + } + }); + + startBackfill(event); + } catch (err) { + const msg = String(err.message || err); + if (msg.includes("404")) showError("Couldn't find that event. Double-check the link or ID."); + else if (msg.includes("401") || msg.includes("403")) showError("This event's roster isn't publicly visible."); + else showError(`Something went wrong reaching BCP: ${msg}`); + } +} + +/* ---- Wire up ---- */ +$("scout-form").addEventListener("submit", e => { + e.preventDefault(); + loadEvent($("event-input").value); +}); + +$("player-filter").addEventListener("input", () => { + if (state) render(); +}); + +$("disposition-toggle").addEventListener("click", () => { + dispositionExpanded = !dispositionExpanded; + if (state) render(); +}); + +$("bcp-token-input").value = getToken(); +$("bcp-token-save").addEventListener("click", () => { + setToken($("bcp-token-input").value.trim()); + if (state) startBackfill(state.event); +}); +$("bcp-token-clear").addEventListener("click", () => { + setToken(""); + $("bcp-token-input").value = ""; + $("bcp-token-status").textContent = "Cleared."; +}); + +/* ---- Boot: seed from ?event= if present ---- */ +const seeded = new URLSearchParams(location.search).get("event"); +if (seeded) { + $("event-input").value = seeded; + loadEvent(seeded); +} diff --git a/sites/scouting/src/style.css b/sites/scouting/src/style.css new file mode 100644 index 0000000..d9825ce --- /dev/null +++ b/sites/scouting/src/style.css @@ -0,0 +1,12 @@ +@import "tailwindcss"; +@plugin "daisyui"; + +/* Data-viz accent (sequential blue, single hue) — dataviz skill palette, blue slot */ +:root[data-theme="dark"] { --viz-accent: #3987e5; } +:root[data-theme="light"] { --viz-accent: #2a78d6; } +html:not([data-theme]) { --viz-accent: #3987e5; } + +.viz-bar { + background: var(--viz-accent); + border-radius: 0 4px 4px 0; +} diff --git a/sites/scouting/vite.config.js b/sites/scouting/vite.config.js new file mode 100644 index 0000000..87b601b --- /dev/null +++ b/sites/scouting/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +export default defineConfig({ + build: { + rollupOptions: { + input: { + index: resolve(__dirname, 'index.html'), + } + } + } +});