diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..cb7b43d --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2095 @@ +{ + "name": "zombie-invasion-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zombie-invasion-backend", + "version": "1.0.0", + "dependencies": { + "@types/better-sqlite3": "^7.6.0", + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "better-sqlite3": "^12.0.0", + "next": "^14.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.4.0" + }, + "devDependencies": { + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.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/@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/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/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.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==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/tinyglobby/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/docs/superpowers/plans/2026-05-29-zombie-invasion-backend.md b/docs/superpowers/plans/2026-05-29-zombie-invasion-backend.md new file mode 100644 index 0000000..76f754c --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-zombie-invasion-backend.md @@ -0,0 +1,3031 @@ +# Zombie Invasion Backend Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) for syntax tracking. + +**Goal:** Build a lightweight Next.js 14 mock backend with SQLite and admin panel for the Zombie Invasion Dota 2 custom game, deployable in a single Docker container. + +**Architecture:** Next.js 14 App Router with a single catch-all API route at `/api/[...path]` that dispatches to domain handlers. Admin panel under `/admin` with cookie-based password auth. SQLite via better-sqlite3 for persistence. All payment endpoints auto-grant instantly. + +**Tech Stack:** Next.js 14, TypeScript, better-sqlite3, Tailwind CSS (admin panel), Docker + +**Location:** All files go in `backend/` directory at the root of this repo. + +--- + +## Task Outline + +### Task 1: Project scaffold +### Task 2: Database layer +### Task 3: Core utilities (auth, seed, router) +### Task 4: Catch-all API route +### Task 5: Player handler +### Task 6: Battle pass handler +### Task 7: Game handler +### Task 8: Payments handler +### Task 9: Leaderboard handler +### Task 10: Cards & decks handler +### Task 11: Equipment handler +### Task 12: Arsenal handler +### Task 13: Marketplace handler +### Task 14: Contracts handler +### Task 15: Admin login & layout +### Task 16: Admin dashboard page +### Task 17: Admin players pages +### Task 18: Admin battle pass pages +### Task 19: Admin matches page +### Task 20: Admin promocodes page +### Task 21: Admin store page +### Task 22: Admin contracts page +### Task 23: Admin arsenal page +### Task 24: Docker setup +### Task 25: Update postman_collection.json + +--- + +### Task 1: Project scaffold + +**Files:** +- Create: `backend/package.json` +- Create: `backend/tsconfig.json` +- Create: `backend/next.config.js` +- Create: `backend/tailwind.config.ts` +- Create: `backend/postcss.config.js` +- Create: `backend/src/app/globals.css` +- Create: `backend/src/app/layout.tsx` +- Create: `backend/src/app/page.tsx` + +- [ ] **Step 1: Create package.json** + +```json +{ + "name": "zombie-invasion-backend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 3000", + "build": "next build", + "start": "next start -p 3000" + }, + "dependencies": { + "next": "^14.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "better-sqlite3": "^11.0.0", + "typescript": "^5.4.0", + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/better-sqlite3": "^7.6.0" + }, + "devDependencies": { + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} +``` + +- [ ] **Step 2: Create tsconfig.json** + +```json +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./src/*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} +``` + +- [ ] **Step 3: Create next.config.js** + +```js +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', +}; + +module.exports = nextConfig; +``` + +- [ ] **Step 4: Create tailwind.config.ts** + +```ts +import type { Config } from 'tailwindcss'; +const config: Config = { + content: ['./src/**/*.{ts,tsx}'], + theme: { extend: {} }, + plugins: [], +}; +export default config; +``` + +- [ ] **Step 5: Create postcss.config.js** + +```js +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; +``` + +- [ ] **Step 6: Create globals.css** + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +- [ ] **Step 7: Create root layout.tsx** + +```tsx +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Zombie Invasion Backend', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +- [ ] **Step 8: Create root page.tsx** + +```tsx +import { redirect } from 'next/navigation'; + +export default function Home() { + redirect('/admin'); +} +``` + +- [ ] **Step 9: Install dependencies only** + +Run: `cd /Users/achmad/Documents/dota/3728427109/backend && npm install` +Expected: Dependencies installed in node_modules/. (Full build happens in Task 14 after all handlers exist.) + +- [ ] **Step 10: Commit** + +```bash +git add backend/package.json backend/tsconfig.json backend/next.config.js backend/tailwind.config.ts backend/postcss.config.js backend/src/app/globals.css backend/src/app/layout.tsx backend/src/app/page.tsx +git commit -m "feat: scaffold Next.js 14 project" +``` + +--- + +### Task 2: Database layer + +**Files:** +- Create: `backend/src/lib/db.ts` +- Create: `backend/src/lib/seed.ts` + +The database layer initializes SQLite with all tables and provides a singleton `db` export for all handlers. + +- [ ] **Step 1: Create db.ts** + +```ts +import Database from 'better-sqlite3'; +import path from 'path'; +import { seedDatabase } from './seed'; + +const DB_PATH = process.env.DB_PATH || path.join(process.cwd(), 'data', 'zombie_invasion.db'); + +let db: Database.Database; + +export function getDb(): Database.Database { + if (!db) { + const fs = require('fs'); + const dir = path.dirname(DB_PATH); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + db = new Database(DB_PATH); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + initSchema(db); + seedDatabase(); + } + return db; +} + +function initSchema(db: Database.Database) { + db.exec(` + CREATE TABLE IF NOT EXISTS players ( + steam_id TEXT PRIMARY KEY, + player_name TEXT NOT NULL, + profile_level INTEGER DEFAULT 1, + free_currency INTEGER DEFAULT 0, + donate_currency INTEGER DEFAULT 0, + dust_currency INTEGER DEFAULT 0, + arcade_pack_credits TEXT DEFAULT '{"standard":0,"premium":0}', + sounds_wheel TEXT DEFAULT '{}', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS game_sessions ( + game_id TEXT PRIMARY KEY, + match_id INTEGER, + session_id TEXT, + status TEXT DEFAULT 'active', + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS game_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + steam_id TEXT NOT NULL, + game_id TEXT, + match_id INTEGER, + result TEXT, + hero TEXT, + hero_level INTEGER, + difficulty TEXT, + duration INTEGER, + kills INTEGER DEFAULT 0, + deaths INTEGER DEFAULT 0, + score INTEGER DEFAULT 0, + outgoing_damage REAL DEFAULT 0, + incoming_damage REAL DEFAULT 0, + items TEXT, + modifiers TEXT, + aghanim_scepter INTEGER DEFAULT 0, + aghanim_shard INTEGER DEFAULT 0, + gold_earned INTEGER DEFAULT 0, + session_id TEXT, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS battle_passes ( + steam_id TEXT PRIMARY KEY, + level INTEGER DEFAULT 0, + experience INTEGER DEFAULT 0, + has_premium INTEGER DEFAULT 0, + claimed_rewards TEXT DEFAULT '[]', + claimed_premium_rewards TEXT DEFAULT '[]', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS battle_pass_quests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + steam_id TEXT NOT NULL, + quest_id TEXT NOT NULL, + type TEXT NOT NULL, + name TEXT, + description TEXT, + progress INTEGER DEFAULT 0, + target INTEGER DEFAULT 1, + completed INTEGER DEFAULT 0, + claimed INTEGER DEFAULT 0, + reward_exp INTEGER DEFAULT 0, + reward_free_currency INTEGER DEFAULT 0, + quality TEXT, + npc TEXT, + target_item TEXT, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS purchases ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + steam_id TEXT NOT NULL, + item_id TEXT NOT NULL, + item_category TEXT, + card_id INTEGER, + price_free INTEGER DEFAULT 0, + price_donate INTEGER DEFAULT 0, + price_dust INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS active_effects ( + steam_id TEXT PRIMARY KEY, + effects TEXT DEFAULT '{}', + updated_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS promo_codes ( + code TEXT PRIMARY KEY, + free_currency INTEGER DEFAULT 0, + donate_currency INTEGER DEFAULT 0, + dust_currency INTEGER DEFAULT 0, + max_uses INTEGER DEFAULT 1, + current_uses INTEGER DEFAULT 0, + expires_at TEXT + ); + + CREATE TABLE IF NOT EXISTS promo_redemptions ( + steam_id TEXT, + code TEXT, + redeemed_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (steam_id, code) + ); + + CREATE TABLE IF NOT EXISTS card_levels ( + steam_id TEXT PRIMARY KEY, + card_levels TEXT DEFAULT '{}', + updated_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS decks ( + steam_id TEXT, + deck_index INTEGER, + name TEXT DEFAULT 'My Deck', + cards TEXT DEFAULT '[]', + updated_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (steam_id, deck_index) + ); + + CREATE TABLE IF NOT EXISTS equipment ( + steam_id TEXT PRIMARY KEY, + equipment TEXT DEFAULT '{}', + updated_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS arsenal_loadouts ( + steam_id TEXT, + hero_name TEXT, + loadout TEXT DEFAULT '{}', + updated_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (steam_id, hero_name) + ); + + CREATE TABLE IF NOT EXISTS arsenal_inventory ( + steam_id TEXT, + instance_id TEXT, + item_name TEXT, + quality TEXT, + upgrade_level INTEGER DEFAULT 0, + serial INTEGER, + global_serial INTEGER, + owner_name TEXT, + pinned INTEGER DEFAULT 0, + favorite INTEGER DEFAULT 0, + stats TEXT DEFAULT '[]', + PRIMARY KEY (steam_id, instance_id) + ); + + CREATE TABLE IF NOT EXISTS arsenal_market_listings ( + listing_id TEXT PRIMARY KEY, + steam_id TEXT NOT NULL, + instance_id TEXT, + item_name TEXT, + quality TEXT, + upgrade_level INTEGER DEFAULT 0, + serial INTEGER, + global_serial INTEGER, + price_free INTEGER DEFAULT 0, + status TEXT DEFAULT 'active', + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS arsenal_market_sales ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + listing_id TEXT, + seller_steam_id TEXT, + buyer_steam_id TEXT, + item_name TEXT, + price_free INTEGER, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS death_sentence_contracts ( + steam_id TEXT PRIMARY KEY, + contracts TEXT DEFAULT '{}', + updated_at TEXT DEFAULT (datetime('now')) + ); + `); +} +``` + +- [ ] **Step 2: Create seed.ts** + +```ts +import { getDb } from './db'; + +export function seedDatabase() { + const db = getDb(); + const count = db.prepare('SELECT COUNT(*) as c FROM promo_codes').get() as { c: number }; + if (count.c > 0) return; + + const insert = db.prepare(` + INSERT INTO promo_codes (code, free_currency, donate_currency, dust_currency, max_uses, expires_at) + VALUES (?, ?, ?, ?, ?, ?) + `); + + const codes = [ + ['WELCOME100', 100, 0, 0, 100, null], + ['ZOMBIE500', 500, 50, 0, 50, null], + ['DONATE100', 0, 100, 0, 20, null], + ['DUST250', 0, 0, 250, 30, null], + ]; + + const tx = db.transaction(() => { + for (const c of codes) { + insert.run(...c); + } + }); + tx(); + + // Seed default quest definitions (will be assigned to players on BP creation) + const seedQuests = [ + { quest_id: 'kill_zombies_1', type: 'kill_zombies', name: 'Zombie Slayer I', description: 'Kill 100 zombies', target: 100, reward_exp: 50, reward_free_currency: 100 }, + { quest_id: 'kill_zombies_2', type: 'kill_zombies', name: 'Zombie Slayer II', description: 'Kill 500 zombies', target: 500, reward_exp: 100, reward_free_currency: 250 }, + { quest_id: 'survive_time_1', type: 'survive_time', name: 'Survivor I', description: 'Survive for 600 seconds', target: 600, reward_exp: 30, reward_free_currency: 50 }, + { quest_id: 'survive_waves_1', type: 'survive_waves', name: 'Wave Breaker I', description: 'Survive 10 waves', target: 10, reward_exp: 40, reward_free_currency: 75 }, + { quest_id: 'buy_black_shop_1', type: 'buy_black_shop', name: 'Black Shopper I', description: 'Buy 5 items from Black Shop', target: 5, reward_exp: 25, reward_free_currency: 50 }, + { quest_id: 'complete_npc_quest_1', type: 'complete_npc_quest', name: 'Helper I', description: 'Complete 3 NPC quests', target: 3, reward_exp: 35, reward_free_currency: 60 }, + { quest_id: 'earn_gold_1', type: 'earn_gold', name: 'Gold Rush I', description: 'Earn 5000 gold', target: 5000, reward_exp: 45, reward_free_currency: 100 }, + ]; + + // Store quest defs in a table or just log them — for now we skip persistence + // since quests are assigned per-player dynamically by the BP handler. + + console.log('Database seeded with promo codes and quest definitions'); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/lib/db.ts backend/src/lib/seed.ts +git commit -m "feat: add SQLite schema and seed data" +``` + +--- + +### Task 3: API router + +**Files:** +- Create: `backend/src/lib/router.ts` + +- [ ] **Step 1: Create router.ts** + +This is the routing engine for the catch-all API route. It maps URL patterns to handler functions. + +```ts +import { NextResponse } from 'next/server'; + +export type HandlerFn = (ctx: HandlerContext) => unknown | Promise; + +export type HandlerContext = { + params: Record; + method: string; + body: unknown; + searchParams: URLSearchParams; +}; + +type RouteEntry = { + pattern: string[]; + methods: string[]; + handler: HandlerFn; +}; + +const routes: RouteEntry[] = []; + +export function route(pattern: string, methods: string[], handler: HandlerFn) { + const parts = pattern.split('/').filter(Boolean); + routes.push({ pattern: parts, methods: methods.map(m => m.toUpperCase()), handler }); +} + +export async function dispatch( + request: Request, + pathSegments: string[], + method: string +): Promise { + for (const entry of routes) { + if (!entry.methods.includes(method)) continue; + const params: Record = {}; + let match = true; + + if (entry.pattern.length !== pathSegments.length) continue; + + for (let i = 0; i < entry.pattern.length; i++) { + const ep = entry.pattern[i]; + const sp = pathSegments[i]; + if (ep.startsWith(':')) { + params[ep.slice(1)] = sp; + } else if (ep !== sp) { + match = false; + break; + } + } + + if (!match) continue; + + let body: unknown = undefined; + const ct = request.headers.get('content-type') || ''; + if (ct.includes('application/json')) { + try { body = await request.json(); } catch { body = undefined; } + } + + const ctx: HandlerContext = { + params, + method, + body, + searchParams: new URL(request.url).searchParams, + }; + + try { + const result = await entry.handler(ctx); + return NextResponse.json(result, { status: 200 }); + } catch (err: any) { + const status = err.status || 500; + return NextResponse.json({ error: err.message || 'Internal error' }, { status }); + } + } + + return NextResponse.json({ error: 'Not found' }, { status: 404 }); +} + +// Helper to create typed errors +export class HttpError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/lib/router.ts +git commit -m "feat: add API router" +``` + +--- + +### Task 4: Catch-all API route + +**Files:** +- Create: `backend/src/app/api/[...path]/route.ts` + +This file registers all route handlers and exports GET/POST/PUT handlers that delegate to the router. + +- [ ] **Step 1: Create the catch-all route** + +```ts +import { dispatch } from '@/lib/router'; +import { NextRequest, NextResponse } from 'next/server'; + +// Import all handlers to register their routes +import '@/lib/handlers/player'; +import '@/lib/handlers/battlepass'; +import '@/lib/handlers/game'; +import '@/lib/handlers/payments'; +import '@/lib/handlers/leaderboard'; +import '@/lib/handlers/cards'; +import '@/lib/handlers/equipment'; +import '@/lib/handlers/arsenal'; +import '@/lib/handlers/marketplace'; +import '@/lib/handlers/contracts'; + +export const dynamic = 'force-dynamic'; + +export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) { + return dispatch(request, params.path, 'GET'); +} + +export async function POST(request: NextRequest, { params }: { params: { path: string[] } }) { + return dispatch(request, params.path, 'POST'); +} + +export async function PUT(request: NextRequest, { params }: { params: { path: string[] } }) { + return dispatch(request, params.path, 'PUT'); +} +``` + +- [ ] **Step 2: Create handlers directory** + +Run: `mkdir -p backend/src/lib/handlers` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/app/api/[...path]/route.ts +git commit -m "feat: add catch-all API route with handler imports" +``` + +--- + +### Task 5: Player handler + +**Files:** +- Create: `backend/src/lib/handlers/player.ts` + +This handler covers all `/player/:steamId/*` endpoints. The Lua client makes many different requests here. + +- [ ] **Step 1: Create player handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// POST /player — Create profile +route('player', ['POST'], (ctx: HandlerContext) => { + const { steam_id, player_name } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id is required'); + const db = getDb(); + const existing = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id) as any; + if (existing) { + // Return existing on conflict (game handles 409 silently) + return existing; + } + db.prepare('INSERT INTO players (steam_id, player_name) VALUES (?, ?)').run(steam_id, player_name || ''); + // Also create battle pass + try { + db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(steam_id); + } catch {} + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id); + return player; +}); + +// GET /player/:steamId — Get profile +route('player/:steamId', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + return { + ...player, + recentGames: [], + stats: { + total_games: 0, + total_wins: 0, + rating: 0, + }, + }; +}); + +// GET /player/:steamId/history — Match history +route('player/:steamId/history', ['GET'], (ctx: HandlerContext) => { + const limit = parseInt(ctx.searchParams.get('limit') || '10'); + const offset = parseInt(ctx.searchParams.get('offset') || '0'); + const db = getDb(); + const games = db.prepare( + 'SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?' + ).all(ctx.params.steamId, limit, offset); + return games; +}); + +// GET /player/:steamId/currency — Get currency +route('player/:steamId/currency', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + return player; +}); + +// PUT /player/:steamId/currency — Save currency +route('player/:steamId/currency', ['PUT'], (ctx: HandlerContext) => { + const { free_currency, donate_currency, dust_currency } = ctx.body as any; + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + db.prepare(` + UPDATE players SET free_currency = ?, donate_currency = ?, dust_currency = ?, updated_at = datetime('now') + WHERE steam_id = ? + `).run( + free_currency ?? player.free_currency, + donate_currency ?? player.donate_currency, + dust_currency ?? player.dust_currency, + ctx.params.steamId + ); + return { success: true }; +}); + +// POST /player/:steamId/currency/give — Grant currency +route('player/:steamId/currency/give', ['POST'], (ctx: HandlerContext) => { + const { free_amount, donate_amount, dust_amount } = ctx.body as any; + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + db.prepare(` + UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, + dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? + `).run( + free_amount || 0, + donate_amount || 0, + dust_amount || 0, + ctx.params.steamId + ); + return { success: true }; +}); + +// POST /player/:steamId/purchases — Record purchase +route('player/:steamId/purchases', ['POST'], (ctx: HandlerContext) => { + const { item_id, item_category, card_id, price_free, price_donate, price_dust } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO purchases (steam_id, item_id, item_category, card_id, price_free, price_donate, price_dust) + VALUES (?, ?, ?, ?, ?, ?, ?) + `).run(ctx.params.steamId, item_id, item_category || 'items', card_id || null, price_free || 0, price_donate || 0, price_dust || 0); + return { success: true }; +}); + +// POST /player/:steamId/promo/redeem — Redeem promo code +route('player/:steamId/promo/redeem', ['POST'], (ctx: HandlerContext) => { + const { code } = ctx.body as any; + if (!code) throw new HttpError(400, 'Code is required'); + const db = getDb(); + const promo = db.prepare('SELECT * FROM promo_codes WHERE code = ?').get(code.toUpperCase()) as any; + if (!promo) throw new HttpError(404, 'Promo code not found'); + if (promo.expires_at && new Date(promo.expires_at) < new Date()) throw new HttpError(400, 'Code expired'); + if (promo.current_uses >= promo.max_uses) throw new HttpError(400, 'Code fully redeemed'); + + // Check if already redeemed by this player + const existing = db.prepare('SELECT * FROM promo_redemptions WHERE steam_id = ? AND code = ?').get(ctx.params.steamId, code.toUpperCase()); + if (existing) throw new HttpError(400, 'Code already redeemed'); + + db.prepare(` + UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, + dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? + `).run(promo.free_currency, promo.donate_currency, promo.dust_currency, ctx.params.steamId); + + db.prepare('UPDATE promo_codes SET current_uses = current_uses + 1 WHERE code = ?').run(code.toUpperCase()); + db.prepare('INSERT INTO promo_redemptions (steam_id, code) VALUES (?, ?)').run(ctx.params.steamId, code.toUpperCase()); + + const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId); + return { success: true, rewards: { free_currency: promo.free_currency, donate_currency: promo.donate_currency, dust_currency: promo.dust_currency }, currency: player }; +}); + +// GET /player/:steamId/sounds_wheel — Get sounds wheel +route('player/:steamId/sounds_wheel', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const player = db.prepare('SELECT sounds_wheel FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + return { sounds_wheel: JSON.parse(player.sounds_wheel || '{}') }; +}); + +// PUT /player/:steamId/sounds_wheel — Save sounds wheel +route('player/:steamId/sounds_wheel', ['PUT'], (ctx: HandlerContext) => { + const { sounds_wheel } = ctx.body as any; + const db = getDb(); + db.prepare("UPDATE players SET sounds_wheel = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify(sounds_wheel || {}), ctx.params.steamId); + return { success: true }; +}); + +// POST /player/:steamId/deal-purchase — Buy a deal +route('player/:steamId/deal-purchase', ['POST'], (ctx: HandlerContext) => { + const { deal_key } = ctx.body as any; + // Auto-grant: mock successful deal purchase + return { success: true, ok: true, item_id: 'deal_' + deal_key, item_category: 'items' }; +}); + +// GET /player/:steamId/active_effects — Get active effects +route('player/:steamId/active_effects', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const row = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(ctx.params.steamId) as any; + return { active_effects: row ? JSON.parse(row.effects) : {} }; +}); + +// PUT /player/:steamId/active_effects — Save active effects +route('player/:steamId/active_effects', ['PUT'], (ctx: HandlerContext) => { + const { active_effects } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO active_effects (steam_id, effects, updated_at) VALUES (?, ?, datetime('now')) + ON CONFLICT(steam_id) DO UPDATE SET effects = ?, updated_at = datetime('now') + `).run(ctx.params.steamId, JSON.stringify(active_effects || {}), JSON.stringify(active_effects || {})); + return { success: true }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/player.ts +git commit -m "feat: add player handler with all endpoints" +``` + +--- + +### Task 6: Battle pass handler + +**Files:** +- Create: `backend/src/lib/handlers/battlepass.ts` + +Covers all `/battlepass/*` endpoints. The game client manages quest logic locally and syncs progress here. + +- [ ] **Step 1: Create battlepass handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +const QUEST_DEFS = [ + { quest_id: 'kill_zombies_1', type: 'kill_zombies', name: 'Zombie Slayer I', description: 'Kill 100 zombies', target: 100, reward_exp: 50, reward_free_currency: 100 }, + { quest_id: 'kill_zombies_2', type: 'kill_zombies', name: 'Zombie Slayer II', description: 'Kill 500 zombies', target: 500, reward_exp: 100, reward_free_currency: 250 }, + { quest_id: 'survive_time_1', type: 'survive_time', name: 'Survivor I', description: 'Survive for 600 seconds', target: 600, reward_exp: 30, reward_free_currency: 50 }, + { quest_id: 'survive_waves_1', type: 'survive_waves', name: 'Wave Breaker I', description: 'Survive 10 waves', target: 10, reward_exp: 40, reward_free_currency: 75 }, + { quest_id: 'buy_black_shop_1', type: 'buy_black_shop', name: 'Black Shopper I', description: 'Buy 5 items from Black Shop', target: 5, reward_exp: 25, reward_free_currency: 50 }, + { quest_id: 'complete_npc_quest_1', type: 'complete_npc_quest', name: 'Helper I', description: 'Complete 3 NPC quests', target: 3, reward_exp: 35, reward_free_currency: 60 }, + { quest_id: 'earn_gold_1', type: 'earn_gold', name: 'Gold Rush I', description: 'Earn 5000 gold', target: 5000, reward_exp: 45, reward_free_currency: 100 }, + { quest_id: 'hero_level_1', type: 'hero_level', name: 'Stronger I', description: 'Reach level 10', target: 10, reward_exp: 30, reward_free_currency: 50 }, + { quest_id: 'cook_grilled_meat_1', type: 'cook_grilled_meat', name: 'Chef I', description: 'Cook grilled meat', target: 1, reward_exp: 20, reward_free_currency: 25 }, + { quest_id: 'use_campfire_1', type: 'use_campfire', name: 'Camper I', description: 'Use campfire 5 times', target: 5, reward_exp: 15, reward_free_currency: 25 }, + { quest_id: 'tip_teammate_1', type: 'tip_teammate', name: 'Friendly I', description: 'Tip teammates 3 times', target: 3, reward_exp: 20, reward_free_currency: 30 }, + { quest_id: 'deal_damage_1', type: 'deal_damage', name: 'Berserker I', description: 'Deal 50000 damage', target: 50000, reward_exp: 60, reward_free_currency: 150 }, + { quest_id: 'collect_item_1', type: 'collect_item', name: 'Collector I', description: 'Collect a rare item', target: 1, reward_exp: 40, reward_free_currency: 80, target_item: 'rare' }, +]; + +// POST /battlepass — Create BP +route('battlepass', ['POST'], (ctx: HandlerContext) => { + const { steam_id } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id required'); + const db = getDb(); + db.prepare(` + INSERT OR IGNORE INTO battle_passes (steam_id, level, experience) VALUES (?, 0, 0) + `).run(steam_id); + + // Assign default quests + const existing = db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE steam_id = ?').get(steam_id) as any; + if (existing.c === 0) { + const insert = db.prepare(` + INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, description, target, reward_exp, reward_free_currency) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const q of QUEST_DEFS) { + insert.run(steam_id, q.quest_id, q.type, q.name, q.description, q.target, q.reward_exp, q.reward_free_currency); + } + } + + return { success: true }; +}); + +// GET /battlepass/:steamId — Get BP data +route('battlepass/:steamId', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + let bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) { + db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(ctx.params.steamId); + bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId); + } + return { + level: bp.level, + experience: bp.experience, + has_premium: bp.has_premium === 1, + claimed_rewards: JSON.parse(bp.claimed_rewards || '[]'), + claimed_premium_rewards: JSON.parse(bp.claimed_premium_rewards || '[]'), + }; +}); + +// GET /battlepass/:steamId/quests — Get quests +route('battlepass/:steamId/quests', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const quests = db.prepare( + 'SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id' + ).all(ctx.params.steamId); + return { quests }; +}); + +// POST /battlepass/:steamId/quests/progress — Sync quest progress +route('battlepass/:steamId/quests/progress', ['POST'], (ctx: HandlerContext) => { + const { quest_id, progress } = ctx.body as any; + if (!quest_id) throw new HttpError(400, 'quest_id required'); + const db = getDb(); + const quest = db.prepare( + 'SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?' + ).get(ctx.params.steamId, quest_id) as any; + if (!quest) { + // Auto-create quest + db.prepare(` + INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, target, progress) + VALUES (?, ?, 'custom', ?, 1, ?) + `).run(ctx.params.steamId, quest_id, quest_id, progress || 0); + return { success: true, completed: false, progress: progress || 0 }; + } + const newProgress = Math.min(progress ?? quest.progress, quest.target); + const completed = newProgress >= quest.target ? 1 : 0; + db.prepare(` + UPDATE battle_pass_quests SET progress = ?, completed = ?, updated_at = datetime('now') + WHERE id = ? + `).run(newProgress, completed, quest.id); + return { success: true, completed: completed === 1, progress: newProgress }; +}); + +// POST /battlepass/:steamId/quests/claim — Claim quest reward +route('battlepass/:steamId/quests/claim', ['POST'], (ctx: HandlerContext) => { + const { quest_id } = ctx.body as any; + if (!quest_id) throw new HttpError(400, 'quest_id required'); + const db = getDb(); + const quest = db.prepare( + 'SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?' + ).get(ctx.params.steamId, quest_id) as any; + if (!quest) throw new HttpError(404, 'Quest not found'); + if (!quest.completed) throw new HttpError(400, 'Quest not completed'); + if (quest.claimed) throw new HttpError(400, 'Already claimed'); + + db.prepare('UPDATE battle_pass_quests SET claimed = 1, updated_at = datetime(\'now\') WHERE id = ?').run(quest.id); + + // Grant rewards + db.prepare(` + UPDATE players SET free_currency = free_currency + ?, updated_at = datetime('now') WHERE steam_id = ? + `).run(quest.reward_free_currency, ctx.params.steamId); + + // Add BP XP + db.prepare(` + UPDATE battle_passes SET experience = experience + ?, updated_at = datetime('now') WHERE steam_id = ? + `).run(quest.reward_exp, ctx.params.steamId); + + const bp = db.prepare('SELECT level, experience FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + + return { + success: true, + reward_exp: quest.reward_exp, + reward_free_currency: quest.reward_free_currency, + new_level: bp.level, + new_experience: bp.experience, + }; +}); + +// POST /battlepass/:steamId/hero-played — Record hero played +route('battlepass/:steamId/hero-played', ['POST'], (ctx: HandlerContext) => { + const { hero_name } = ctx.body as any; + return { success: true }; +}); + +// POST /battlepass/:steamId/claim — Claim BP level reward (free) +route('battlepass/:steamId/claim', ['POST'], (ctx: HandlerContext) => { + const { steam_id, level } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + let claimed = JSON.parse(bp.claimed_rewards || '[]'); + if (!claimed.includes(level)) { + claimed.push(level); + db.prepare("UPDATE battle_passes SET claimed_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify(claimed), ctx.params.steamId); + } + return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: 0 } }; +}); + +// POST /battlepass/:steamId/claim-premium — Claim BP premium reward +route('battlepass/:steamId/claim-premium', ['POST'], (ctx: HandlerContext) => { + const { steam_id, level } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + let claimed = JSON.parse(bp.claimed_premium_rewards || '[]'); + if (!claimed.includes(level)) { + claimed.push(level); + db.prepare("UPDATE battle_passes SET claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify(claimed), ctx.params.steamId); + } + return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: level * 100 } }; +}); + +// POST /battlepass/:steamId/claim-all — Claim all rewards +route('battlepass/:steamId/claim-all', ['POST'], (ctx: HandlerContext) => { + const { steam_id } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + + const unclaimedFree: number[] = []; + const unclaimedPremium: number[] = []; + const claimedFree = JSON.parse(bp.claimed_rewards || '[]'); + const claimedPremium = JSON.parse(bp.claimed_premium_rewards || '[]'); + + for (let lvl = 1; lvl <= bp.level; lvl++) { + if (!claimedFree.includes(lvl)) unclaimedFree.push(lvl); + if (bp.has_premium && !claimedPremium.includes(lvl)) unclaimedPremium.push(lvl); + } + + db.prepare("UPDATE battle_passes SET claimed_rewards = ?, claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify([...claimedFree, ...unclaimedFree]), + JSON.stringify([...claimedPremium, ...unclaimedPremium]), + ctx.params.steamId); + + return { + success: true, + free_levels: unclaimedFree, + premium_levels: unclaimedPremium, + currency_granted: { free_currency: unclaimedFree.length * 250 + unclaimedPremium.length * 250, donate_currency: unclaimedPremium.length * 100 }, + }; +}); + +// POST /battlepass/:steamId/buy-premium — Buy premium BP +route('battlepass/:steamId/buy-premium', ['POST'], (ctx: HandlerContext) => { + const db = getDb(); + db.prepare("UPDATE battle_passes SET has_premium = 1, updated_at = datetime('now') WHERE steam_id = ?") + .run(ctx.params.steamId); + return { success: true }; +}); + +// POST /battlepass/:steamId/addexp — Add BP XP +route('battlepass/:steamId/addexp', ['POST'], (ctx: HandlerContext) => { + const { experience } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + + const newExp = bp.experience + (experience || 0); + const levelUp = Math.floor(newExp / 1000); + const newLevel = bp.level + levelUp; + const remainder = newExp % 1000; + + db.prepare(` + UPDATE battle_passes SET experience = ?, level = ?, updated_at = datetime('now') WHERE steam_id = ? + `).run(remainder, newLevel, ctx.params.steamId); + + return { level: newLevel, experience: remainder, level_up: levelUp > 0 }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/battlepass.ts +git commit -m "feat: add battle pass handler with quests, rewards, XP" +``` + +--- + +### Task 7: Game handler + +**Files:** +- Create: `backend/src/lib/handlers/game.ts` + +- [ ] **Step 1: Create game handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// POST /game/start — Register game start +route('game/start', ['POST'], (ctx: HandlerContext) => { + const { steam_id, hero, hero_level, difficulty, player_name, match_id, session_id, session_participants } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id required'); + const db = getDb(); + + // Generate IDs + const gameId = `game_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + const newMatchId = match_id || Math.floor(Math.random() * 100000000); + + // Store session + db.prepare(` + INSERT OR REPLACE INTO game_sessions (game_id, match_id, session_id, status) + VALUES (?, ?, ?, 'active') + `).run(gameId, newMatchId, session_id || ''); + + return { game_id: gameId, match_id: newMatchId }; +}); + +// POST /game/heartbeat — Match heartbeat +route('game/heartbeat', ['POST'], (ctx: HandlerContext) => { + return { success: true }; +}); + +// POST /game — Save game result +route('game', ['POST'], (ctx: HandlerContext) => { + const { + steam_id, result, duration, kills, deaths, score, outgoing_damage, incoming_damage, + hero, hero_level, items, modifiers, aghanim_scepter, aghanim_shard, gold_earned, + difficulty, session_id, game_id, + } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id required'); + + const db = getDb(); + db.prepare(` + INSERT INTO game_history (steam_id, game_id, result, duration, kills, deaths, score, + outgoing_damage, incoming_damage, hero, hero_level, items, modifiers, + aghanim_scepter, aghanim_shard, gold_earned, difficulty, session_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + steam_id, game_id || null, result || 'loss', duration || 0, kills || 0, deaths || 0, + score || 0, outgoing_damage || 0, incoming_damage || 0, hero || '', hero_level || 1, + items || '', modifiers || '', aghanim_scepter ? 1 : 0, aghanim_shard ? 1 : 0, + gold_earned || 0, difficulty || 'normal', session_id || '' + ); + + // Update session status + if (game_id) { + db.prepare("UPDATE game_sessions SET status = 'completed' WHERE game_id = ?").run(game_id); + } + + return { success: true }; +}); + +// GET /game/:id/players — Get game players +route('game/:id/players', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const session = db.prepare('SELECT * FROM game_sessions WHERE game_id = ? OR match_id = ?') + .get(ctx.params.id, parseInt(ctx.params.id) || 0) as any; + if (!session) return { players: [] }; + + const players = db.prepare( + 'SELECT DISTINCT steam_id, hero, hero_level, result FROM game_history WHERE match_id = ? OR game_id = ?' + ).all(session.match_id, session.game_id); + + return { party_players: players, players }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/game.ts +git commit -m "feat: add game/match handler" +``` + +--- + +### Task 8: Payments handler + +**Files:** +- Create: `backend/src/lib/handlers/payments.ts` + +Auto-grant — no real payment processing. + +- [ ] **Step 1: Create payments handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// POST /payments/robokassa/link — Auto-grant purchased currency +route('payments/robokassa/link', ['POST'], (ctx: HandlerContext) => { + const { steam_id, amount_rub } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id required'); + const db = getDb(); + + // Convert rubles to donate shards (e.g., 1 RUB = 10 shards) + const donateShards = (amount_rub || 100) * 10; + db.prepare('UPDATE players SET donate_currency = donate_currency + ?, updated_at = datetime(\'now\') WHERE steam_id = ?') + .run(donateShards, steam_id); + + return { + ok: true, + payment_url: '', + donate_shards: donateShards, + inv_id: Math.floor(Math.random() * 100000), + }; +}); + +// POST /payments/bundles/link — Auto-grant bundle +route('payments/bundles/link', ['POST'], (ctx: HandlerContext) => { + const { steam_id, bundle_id } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id required'); + const db = getDb(); + + db.prepare('UPDATE players SET free_currency = free_currency + 500, donate_currency = donate_currency + 200, updated_at = datetime(\'now\') WHERE steam_id = ?') + .run(steam_id); + + return { + ok: true, + payment_url: '', + inv_id: Math.floor(Math.random() * 100000), + message: 'Bundle granted', + }; +}); + +// GET /payments/deals?steam_id= — Get deals catalog +route('payments/deals', ['GET'], (ctx: HandlerContext) => { + return { + ok: true, + bundles: [ + { id: 'starter_bundle', name: 'Starter Pack', description: 'Get started with 500 shards', price_free: 0, price_donate: 0, items: [{ item_id: 'starter_pack', name: 'Starter Pack' }] }, + { id: 'hero_bundle_1', name: 'Hero Bundle I', description: 'Unlock a random hero', price_free: 1000, price_donate: 0, items: [{ item_id: 'hero_bundle_1', name: 'Hero Bundle' }] }, + ], + daily: { available: true, items: [] }, + weekly: { available: true, items: [] }, + player_created_at_unix: Math.floor(Date.now() / 1000), + }; +}); +``` + +--- + +### Task 9: Leaderboard handler + +**Files:** +- Create: `backend/src/lib/handlers/leaderboard.ts` + +- [ ] **Step 1: Create leaderboard handler** + +```ts +import { route, HandlerContext } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// GET /leaderboard?limit=&offset=&board= +route('leaderboard', ['GET'], (ctx: HandlerContext) => { + const limit = parseInt(ctx.searchParams.get('limit') || '20'); + const offset = parseInt(ctx.searchParams.get('offset') || '0'); + const board = ctx.searchParams.get('board') || 'rating'; + const db = getDb(); + + let rows: any[]; + if (board === 'wealth') { + rows = db.prepare( + 'SELECT steam_id, player_name, (free_currency + donate_currency) as score, free_currency, donate_currency FROM players ORDER BY score DESC LIMIT ? OFFSET ?' + ).all(limit, offset); + } else { + // rating board: based on wins + rows = db.prepare(` + SELECT p.steam_id, p.player_name, COUNT(CASE WHEN gh.result = 'win' THEN 1 END) as wins, + COUNT(gh.id) as total_games, + (COUNT(CASE WHEN gh.result = 'win' THEN 1 END) * 100.0 / MAX(COUNT(gh.id), 1)) as win_rate + FROM players p LEFT JOIN game_history gh ON p.steam_id = gh.steam_id + GROUP BY p.steam_id ORDER BY wins DESC LIMIT ? OFFSET ? + `).all(limit, offset); + } + + return { + leaderboard: rows, + total: (db.prepare('SELECT COUNT(*) as c FROM players').get() as any).c, + board, + }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/leaderboard.ts +git commit -m "feat: add leaderboard handler" +``` + +--- + +### Task 10: Cards & decks handler + +**Files:** +- Create: `backend/src/lib/handlers/cards.ts` + +- [ ] **Step 1: Create cards handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// GET /player/:steamId/card-levels +route('player/:steamId/card-levels', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const row = db.prepare('SELECT card_levels FROM card_levels WHERE steam_id = ?').get(ctx.params.steamId) as any; + return { card_levels: row ? JSON.parse(row.card_levels) : {} }; +}); + +// PUT /player/:steamId/card-levels +route('player/:steamId/card-levels', ['PUT'], (ctx: HandlerContext) => { + const { card_levels } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO card_levels (steam_id, card_levels, updated_at) VALUES (?, ?, datetime('now')) + ON CONFLICT(steam_id) DO UPDATE SET card_levels = ?, updated_at = datetime('now') + `).run(ctx.params.steamId, JSON.stringify(card_levels || {}), JSON.stringify(card_levels || {})); + return { success: true }; +}); + +// GET /player/:steamId/decks +route('player/:steamId/decks', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const decks = db.prepare('SELECT * FROM decks WHERE steam_id = ? ORDER BY deck_index').all(ctx.params.steamId); + return decks.map((d: any) => ({ ...d, cards: JSON.parse(d.cards || '[]') })); +}); + +// GET /player/:steamId/decks/:index +route('player/:steamId/decks/:index', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const deck = db.prepare('SELECT * FROM decks WHERE steam_id = ? AND deck_index = ?').get(ctx.params.steamId, ctx.params.index) as any; + if (!deck) return { name: 'New Deck', cards: [] }; + return { ...deck, cards: JSON.parse(deck.cards || '[]') }; +}); + +// PUT /player/:steamId/decks/:index +route('player/:steamId/decks/:index', ['PUT'], (ctx: HandlerContext) => { + const { name, cards } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO decks (steam_id, deck_index, name, cards, updated_at) VALUES (?, ?, ?, ?, datetime('now')) + ON CONFLICT(steam_id, deck_index) DO UPDATE SET name = ?, cards = ?, updated_at = datetime('now') + `).run(ctx.params.steamId, parseInt(ctx.params.index), name || 'My Deck', JSON.stringify(cards || []), name || 'My Deck', JSON.stringify(cards || [])); + return { success: true }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/cards.ts +git commit -m "feat: add cards and decks handler" +``` + +--- + +### Task 11: Equipment handler + +**Files:** +- Create: `backend/src/lib/handlers/equipment.ts` + +- [ ] **Step 1: Create equipment handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// GET /player/:steamId/equipment +route('player/:steamId/equipment', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const row = db.prepare('SELECT equipment FROM equipment WHERE steam_id = ?').get(ctx.params.steamId) as any; + return { equipment: row ? JSON.parse(row.equipment) : {} }; +}); + +// PUT /player/:steamId/equipment +route('player/:steamId/equipment', ['PUT'], (ctx: HandlerContext) => { + const { equipment } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO equipment (steam_id, equipment, updated_at) VALUES (?, ?, datetime('now')) + ON CONFLICT(steam_id) DO UPDATE SET equipment = ?, updated_at = datetime('now') + `).run(ctx.params.steamId, JSON.stringify(equipment || {}), JSON.stringify(equipment || {})); + return { success: true }; +}); + +// POST /player/:steamId/equipment/drop +route('player/:steamId/equipment/drop', ['POST'], (ctx: HandlerContext) => { + return { success: true }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/equipment.ts +git commit -m "feat: add equipment handler" +``` + +--- + +### Task 12: Arsenal handler + +**Files:** +- Create: `backend/src/lib/handlers/arsenal.ts` + +- [ ] **Step 1: Create arsenal handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// GET /player/:steamId/arsenal_loadouts +route('player/:steamId/arsenal_loadouts', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const rows = db.prepare('SELECT * FROM arsenal_loadouts WHERE steam_id = ?').all(ctx.params.steamId); + const loadouts: Record = {}; + for (const r of rows as any[]) { + loadouts[r.hero_name] = JSON.parse(r.loadout); + } + return { arsenal_loadouts: loadouts }; +}); + +// PUT /player/:steamId/arsenal_loadouts +route('player/:steamId/arsenal_loadouts', ['PUT'], (ctx: HandlerContext) => { + const { arsenal_loadouts } = ctx.body as any; + if (!arsenal_loadouts) throw new HttpError(400, 'arsenal_loadouts required'); + const db = getDb(); + const upsert = db.prepare(` + INSERT INTO arsenal_loadouts (steam_id, hero_name, loadout, updated_at) VALUES (?, ?, ?, datetime('now')) + ON CONFLICT(steam_id, hero_name) DO UPDATE SET loadout = ?, updated_at = datetime('now') + `); + const tx = db.transaction(() => { + for (const [hero, loadout] of Object.entries(arsenal_loadouts)) { + upsert.run(ctx.params.steamId, hero, JSON.stringify(loadout), JSON.stringify(loadout)); + } + }); + tx(); + return { success: true }; +}); + +// GET /player/:steamId/arsenal_inventory +route('player/:steamId/arsenal_inventory', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const items = db.prepare('SELECT * FROM arsenal_inventory WHERE steam_id = ?').all(ctx.params.steamId); + const instances: Record = {}; + for (const item of items as any[]) { + instances[item.instance_id] = { + instanceId: item.instance_id, + itemName: item.item_name, + quality: item.quality, + upgradeLevel: item.upgrade_level, + serial: item.serial, + globalSerial: item.global_serial, + ownerName: item.owner_name, + pinned: !!item.pinned, + favorite: !!item.favorite, + stats: JSON.parse(item.stats || '[]'), + }; + } + return { arsenal_inventory: { instances } }; +}); + +// PUT /player/:steamId/arsenal_inventory +route('player/:steamId/arsenal_inventory', ['PUT'], (ctx: HandlerContext) => { + const { arsenal_inventory } = ctx.body as any; + if (!arsenal_inventory || !arsenal_inventory.instances) throw new HttpError(400, 'arsenal_inventory.instances required'); + const db = getDb(); + const upsert = db.prepare(` + INSERT INTO arsenal_inventory (steam_id, instance_id, item_name, quality, upgrade_level, serial, global_serial, owner_name, pinned, favorite, stats) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(steam_id, instance_id) DO UPDATE SET + item_name = excluded.item_name, quality = excluded.quality, upgrade_level = excluded.upgrade_level, + serial = excluded.serial, global_serial = excluded.global_serial, owner_name = excluded.owner_name, + pinned = excluded.pinned, favorite = excluded.favorite, stats = excluded.stats + `); + const tx = db.transaction(() => { + for (const [instId, inst] of Object.entries(arsenal_inventory.instances)) { + const i = inst as any; + upsert.run(ctx.params.steamId, instId, i.itemName || i.item_name, i.quality, i.upgradeLevel || i.upgrade_level || 0, + i.serial, i.globalSerial || i.global_serial, i.ownerName || i.owner_name || '', + i.pinned ? 1 : 0, i.favorite ? 1 : 0, JSON.stringify(i.stats || [])); + } + }); + tx(); + return { success: true }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/arsenal.ts +git commit -m "feat: add arsenal handler (loadouts + inventory)" +``` + +--- + +### Task 13: Marketplace handler + +**Files:** +- Create: `backend/src/lib/handlers/marketplace.ts` + +- [ ] **Step 1: Create marketplace handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// GET /arsenal_market/listings — Public listings +route('arsenal_market/listings', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const listings = db.prepare( + "SELECT * FROM arsenal_market_listings WHERE status = 'active' ORDER BY created_at DESC" + ).all(); + return listings; +}); + +// GET /player/:steamId/arsenal_market/my_listings +route('player/:steamId/arsenal_market/my_listings', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const listings = db.prepare( + "SELECT * FROM arsenal_market_listings WHERE steam_id = ? AND status = 'active' ORDER BY created_at DESC" + ).all(ctx.params.steamId); + return listings; +}); + +// GET /player/:steamId/arsenal_market/slots +route('player/:steamId/arsenal_market/slots', ['GET'], (ctx: HandlerContext) => { + return { slots: 5, used: 0 }; +}); + +// GET /player/:steamId/arsenal_market/sales +route('player/:steamId/arsenal_market/sales', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const sales = db.prepare( + 'SELECT * FROM arsenal_market_sales WHERE seller_steam_id = ? ORDER BY created_at DESC' + ).all(ctx.params.steamId); + return sales; +}); + +// POST /player/:steamId/arsenal_market/create +route('player/:steamId/arsenal_market/create', ['POST'], (ctx: HandlerContext) => { + const { instance_id, item_name, quality, upgrade_level, serial, global_serial, price_free } = ctx.body as any; + const listingId = `list_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + const db = getDb(); + db.prepare(` + INSERT INTO arsenal_market_listings (listing_id, steam_id, instance_id, item_name, quality, upgrade_level, serial, global_serial, price_free, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active') + `).run(listingId, ctx.params.steamId, instance_id || '', item_name || 'Unknown', quality || 'common', + upgrade_level || 0, serial || 0, global_serial || 0, price_free || 0); + return { success: true, listing_id: listingId }; +}); + +// POST /player/:steamId/arsenal_market/buy +route('player/:steamId/arsenal_market/buy', ['POST'], (ctx: HandlerContext) => { + const { listing_id } = ctx.body as any; + if (!listing_id) throw new HttpError(400, 'listing_id required'); + const db = getDb(); + const listing = db.prepare('SELECT * FROM arsenal_market_listings WHERE listing_id = ?').get(listing_id) as any; + if (!listing) throw new HttpError(404, 'Listing not found'); + if (listing.status !== 'active') throw new HttpError(400, 'Listing not active'); + + db.prepare("UPDATE arsenal_market_listings SET status = 'sold' WHERE listing_id = ?").run(listing_id); + db.prepare(` + INSERT INTO arsenal_market_sales (listing_id, seller_steam_id, buyer_steam_id, item_name, price_free) + VALUES (?, ?, ?, ?, ?) + `).run(listing_id, listing.steam_id, ctx.params.steamId, listing.item_name, listing.price_free); + return { success: true }; +}); + +// POST /player/:steamId/arsenal_market/cancel +route('player/:steamId/arsenal_market/cancel', ['POST'], (ctx: HandlerContext) => { + const { listing_id } = ctx.body as any; + if (!listing_id) throw new HttpError(400, 'listing_id required'); + const db = getDb(); + db.prepare("UPDATE arsenal_market_listings SET status = 'cancelled' WHERE listing_id = ? AND steam_id = ?") + .run(listing_id, ctx.params.steamId); + return { success: true }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/marketplace.ts +git commit -m "feat: add marketplace handler" +``` + +--- + +### Task 14: Contracts handler + +**Files:** +- Create: `backend/src/lib/handlers/contracts.ts` + +- [ ] **Step 1: Create contracts handler** + +```ts +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// GET /player/:steamId/death_sentence_contracts +route('player/:steamId/death_sentence_contracts', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const row = db.prepare('SELECT contracts FROM death_sentence_contracts WHERE steam_id = ?').get(ctx.params.steamId) as any; + return { death_sentence_contracts: row ? JSON.parse(row.contracts) : { roster: [] } }; +}); + +// PUT /player/:steamId/death_sentence_contracts +route('player/:steamId/death_sentence_contracts', ['PUT'], (ctx: HandlerContext) => { + const { death_sentence_contracts } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO death_sentence_contracts (steam_id, contracts, updated_at) VALUES (?, ?, datetime('now')) + ON CONFLICT(steam_id) DO UPDATE SET contracts = ?, updated_at = datetime('now') + `).run(ctx.params.steamId, JSON.stringify(death_sentence_contracts || {}), JSON.stringify(death_sentence_contracts || {})); + return { success: true }; +}); +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/src/lib/handlers/contracts.ts +git commit -m "feat: add contracts handler" +``` + +- [ ] **Step 3: Verify the full build** + +Run: `cd /Users/achmad/Documents/dota/3728427109/backend && npx next build` +Expected: Build succeeds. All handlers compile and register their routes. + +--- + +### Task 15: Admin login & layout + +**Files:** +- Create: `backend/src/app/admin/layout.tsx` +- Create: `backend/src/app/admin/login/page.tsx` + +- [ ] **Step 1: Create admin layout (with sidebar + auth check)** + +```tsx +'use client'; +import { usePathname, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +const NAV = [ + { href: '/admin', label: 'Dashboard' }, + { href: '/admin/players', label: 'Players' }, + { href: '/admin/battlepass', label: 'Battle Pass' }, + { href: '/admin/matches', label: 'Matches' }, + { href: '/admin/promocodes', label: 'Promo Codes' }, + { href: '/admin/store', label: 'Store' }, + { href: '/admin/contracts', label: 'Contracts' }, + { href: '/admin/arsenal', label: 'Arsenal' }, +]; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + const [authed, setAuthed] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (pathname === '/admin/login') { + setLoading(false); + return; + } + fetch('/api/admin/check') + .then(r => r.json()) + .then(d => { + if (d.authenticated) setAuthed(true); + else router.push('/admin/login'); + }) + .catch(() => router.push('/admin/login')) + .finally(() => setLoading(false)); + }, [pathname, router]); + + if (loading) return
Loading...
; + if (pathname === '/admin/login') return <>{children}; + if (!authed) return null; + + return ( +
+ +
{children}
+
+ ); +} +``` + +- [ ] **Step 2: Create login page** + +```tsx +'use client'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function LoginPage() { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + const res = await fetch('/api/admin/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + const data = await res.json(); + if (data.success) router.push('/admin'); + else setError(data.error || 'Login failed'); + }; + + return ( +
+
+

Admin Login

+ {error &&

{error}

} + setPassword(e.target.value)} + placeholder="Password" + className="w-full px-3 py-2 bg-gray-700 rounded mb-4 text-white" + autoFocus + /> + +
+
+ ); +} +``` + +- [ ] **Step 3: Add admin API endpoints** + +Append to `backend/src/lib/handlers/player.ts`: + +```ts +// — Admin auth endpoints — + +route('admin/login', ['POST'], (ctx: HandlerContext) => { + const { password } = ctx.body as any; + // Simple in-memory cookie auth via API + const { verifyPassword, createAdminSession } = require('@/lib/auth'); + if (!verifyPassword(password)) { + return NextResponse.json({ success: false, error: 'Invalid password' }, { status: 401 }); + } + createAdminSession(); + return { success: true }; +}); + +route('admin/check', ['GET'], () => { + const { checkAdminAuth } = require('@/lib/auth'); + return { authenticated: checkAdminAuth() }; +}); + +route('admin/logout', ['GET'], () => { + const { clearAdminSession } = require('@/lib/auth'); + clearAdminSession(); + return { success: true }; +}); +``` + +Wait — these don't work in the router because we can't use `NextResponse.json` in the handler return. The router already wraps in `NextResponse.json`. Let me fix the admin login to not need NextResponse: + +Actually, the issue is that the router always wraps in NextResponse.json with status 200. For admin login on wrong password, we want a 401. The router's error handling only catches HttpError. Let me adjust. + +Better approach: keep admin login in a separate standard Next.js route handler instead of using the catch-all. Create a dedicated file for admin API routes. + +- [ ] **Step 3 (revised): Create admin API routes as standard Next.js routes** + +Create `backend/src/app/api/admin/login/route.ts`: + +```ts +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + const { password } = await request.json(); + const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin'; + if (password !== ADMIN_PASSWORD) { + return NextResponse.json({ success: false, error: 'Invalid password' }, { status: 401 }); + } + const response = NextResponse.json({ success: true }); + response.cookies.set('admin_session', 'authenticated', { + httpOnly: true, secure: false, sameSite: 'lax', path: '/admin', maxAge: 86400, + }); + return response; +} +``` + +Create `backend/src/app/api/admin/check/route.ts`: + +```ts +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +export async function GET() { + const store = cookies(); + const authed = store.get('admin_session')?.value === 'authenticated'; + return NextResponse.json({ authenticated: !!authed }); +} +``` + +Create `backend/src/app/api/admin/logout/route.ts`: + +```ts +import { NextResponse } from 'next/server'; + +export async function GET() { + const response = NextResponse.json({ success: true }); + response.cookies.delete('admin_session'); + return response; +} +``` + +Since admin routes don't start with `api/[...path]`, they'll be matched by these specific route files before the catch-all. + +- [ ] **Step 4: Commit** + +```bash +git add backend/src/app/admin/layout.tsx backend/src/app/admin/login/page.tsx +git add backend/src/app/api/admin/login/route.ts backend/src/app/api/admin/check/route.ts backend/src/app/api/admin/logout/route.ts +git commit -m "feat: add admin layout with sidebar and login" +``` + +--- + +### Task 16: Admin dashboard page + +**Files:** +- Create: `backend/src/app/admin/page.tsx` + +- [ ] **Step 1: Create dashboard page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; + +type Stats = { + players: number; + games: number; + activeBps: number; + questsCompleted: number; +}; + +export default function DashboardPage() { + const [stats, setStats] = useState(null); + + useEffect(() => { + fetch('/api/admin/stats') + .then(r => r.json()) + .then(setStats) + .catch(() => {}); + }, []); + + if (!stats) return
Loading stats...
; + + const cards = [ + { label: 'Players', value: stats.players, color: 'text-blue-400' }, + { label: 'Games Played', value: stats.games, color: 'text-green-400' }, + { label: 'Active Battle Passes', value: stats.activeBps, color: 'text-amber-400' }, + { label: 'Quests Completed', value: stats.questsCompleted, color: 'text-purple-400' }, + ]; + + return ( +
+

Dashboard

+
+ {cards.map(c => ( +
+
{c.label}
+
{c.value}
+
+ ))} +
+
+ ); +} +``` + +- [ ] **Step 2: Create admin stats API route** + +Create `backend/src/app/api/admin/stats/route.ts`: + +```ts +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const players = (db.prepare('SELECT COUNT(*) as c FROM players').get() as any).c; + const games = (db.prepare('SELECT COUNT(*) as c FROM game_history').get() as any).c; + const activeBps = (db.prepare('SELECT COUNT(*) as c FROM battle_passes').get() as any).c; + const questsCompleted = (db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE completed = 1').get() as any).c; + + return NextResponse.json({ players, games, activeBps, questsCompleted }); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/app/admin/page.tsx backend/src/app/api/admin/stats/route.ts +git commit -m "feat: add admin dashboard with stats" +``` + +--- + +### Task 17: Admin players pages + +**Files:** +- Create: `backend/src/app/admin/players/page.tsx` +- Create: `backend/src/app/admin/players/[steamId]/page.tsx` +- Create: `backend/src/app/api/admin/players/route.ts` (search/list) +- Create: `backend/src/app/api/admin/players/[steamId]/route.ts` (get/update) + +- [ ] **Step 1: Create admin players list page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; + +export default function PlayersListPage() { + const [players, setPlayers] = useState([]); + const [search, setSearch] = useState(''); + + useEffect(() => { + fetch('/api/admin/players') + .then(r => r.json()) + .then(setPlayers) + .catch(() => {}); + }, []); + + const filtered = players.filter(p => + p.steam_id?.includes(search) || p.player_name?.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+

Players

+ setSearch(e.target.value)} + className="w-full max-w-md px-3 py-2 bg-gray-800 rounded mb-4 text-white" + /> +
+ + + + + + + + + + + + + {filtered.map(p => ( + window.location.href = `/admin/players/${p.steam_id}`}> + + + + + + + + ))} + +
Steam IDNameLevelFreeDonateDust
{p.steam_id}{p.player_name}{p.profile_level}{p.free_currency}{p.donate_currency}{p.dust_currency}
+
+
+ ); +} +``` + +- [ ] **Step 2: Create admin players API route** + +```ts +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const players = db.prepare('SELECT * FROM players ORDER BY updated_at DESC LIMIT 100').all(); + return NextResponse.json(players); +} +``` + +- [ ] **Step 3: Create admin player detail API route** + +`backend/src/app/api/admin/players/[steamId]/route.ts`: + +```ts +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET(request: NextRequest, { params }: { params: { steamId: string } }) { + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(params.steamId); + if (!player) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); + const purchases = db.prepare('SELECT * FROM purchases WHERE steam_id = ? ORDER BY created_at DESC LIMIT 50').all(params.steamId); + const matches = db.prepare('SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT 20').all(params.steamId); + const effects = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(params.steamId) as any; + + return NextResponse.json({ player, battlePass: bp, purchases, matches, activeEffects: effects ? JSON.parse(effects.effects) : {} }); +} + +export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { + const body = await request.json(); + const db = getDb(); + const fields: string[] = []; + const values: any[] = []; + + for (const key of ['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency']) { + if (body[key] !== undefined) { + fields.push(`${key} = ?`); + values.push(body[key]); + } + } + if (body.sounds_wheel !== undefined) { + fields.push('sounds_wheel = ?'); + values.push(JSON.stringify(body.sounds_wheel)); + } + if (fields.length === 0) return NextResponse.json({ error: 'No fields to update' }, { status: 400 }); + + fields.push("updated_at = datetime('now')"); + values.push(params.steamId); + db.prepare(`UPDATE players SET ${fields.join(', ')} WHERE steam_id = ?`).run(...values); + + return NextResponse.json({ success: true }); +} +``` + +- [ ] **Step 4: Create player detail page** + +`backend/src/app/admin/players/[steamId]/page.tsx`: + +```tsx +'use client'; +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; + +export default function PlayerDetailPage() { + const { steamId } = useParams(); + const router = useRouter(); + const [data, setData] = useState(null); + const [form, setForm] = useState({}); + const [msg, setMsg] = useState(''); + + useEffect(() => { + fetch(`/api/admin/players/${steamId}`) + .then(r => r.json()) + .then(d => { + setData(d); + setForm(d.player || {}); + }) + .catch(() => router.push('/admin/players')); + }, [steamId, router]); + + const save = async () => { + const res = await fetch(`/api/admin/players/${steamId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + const result = await res.json(); + setMsg(result.success ? 'Saved!' : 'Error: ' + (result.error || '')); + }; + + if (!data) return
Loading...
; + + return ( +
+ ← Back to Players +

Player: {data.player?.player_name}

+

Steam ID: {steamId}

+ + {msg &&

{msg}

} + +
+
+

Profile

+
+ {['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency'].map(f => ( +
+ + setForm({ ...form, [f]: e.target.value })} + className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" + /> +
+ ))} +
+ +
+ +
+

Battle Pass

+ {data.battlePass && ( +
+

Level: {data.battlePass.level}

+

XP: {data.battlePass.experience}

+

Premium: {data.battlePass.has_premium ? 'Yes' : 'No'}

+ Edit BP → +
+ )} +
+ +
+

Recent Purchases ({data.purchases?.length})

+
+ {data.purchases?.map((p: any, i: number) => ( +
{p.item_id} ({p.item_category})
+ ))} + {!data.purchases?.length &&

None

} +
+
+ +
+

Recent Matches ({data.matches?.length})

+
+ {data.matches?.map((m: any, i: number) => ( +
+ {m.hero} — {m.result} + ({m.difficulty}) +
+ ))} + {!data.matches?.length &&

None

} +
+
+
+
+ ); +} +``` + +- [ ] **Step 5: Commit** + +```bash +git add backend/src/app/admin/players/page.tsx backend/src/app/admin/players/\[steamId\]/page.tsx +git add backend/src/app/api/admin/players/route.ts backend/src/app/api/admin/players/\[steamId\]/route.ts +git commit -m "feat: add admin players pages (list + detail)" +``` + +--- + +### Task 18: Admin battle pass pages + +**Files:** +- Create: `backend/src/app/admin/battlepass/page.tsx` +- Create: `backend/src/app/admin/battlepass/[steamId]/page.tsx` + +- [ ] **Step 1: Create BP overview page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; + +export default function BattlePassListPage() { + const [bps, setBps] = useState([]); + + useEffect(() => { + fetch('/api/admin/battlepass') + .then(r => r.json()) + .then(setBps) + .catch(() => {}); + }, []); + + return ( +
+

Battle Passes

+
+ + + + + + + + + + + {bps.map(bp => ( + + + + + + + ))} + +
Steam IDLevelXPPremium
+ {bp.steam_id} + {bp.level}{bp.experience}{bp.has_premium ? 'Yes' : 'No'}
+
+
+ ); +} +``` + +- [ ] **Step 2: Create BP detail/edit page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; + +export default function BattlePassDetailPage() { + const { steamId } = useParams(); + const [bp, setBp] = useState(null); + const [quests, setQuests] = useState([]); + const [editLevel, setEditLevel] = useState(0); + const [editXp, setEditXp] = useState(0); + const [editPremium, setEditPremium] = useState(false); + const [msg, setMsg] = useState(''); + + const load = () => { + fetch(`/api/admin/battlepass/${steamId}`) + .then(r => r.json()) + .then(d => { + setBp(d.battlePass); + setQuests(d.quests || []); + setEditLevel(d.battlePass?.level || 0); + setEditXp(d.battlePass?.experience || 0); + setEditPremium(d.battlePass?.has_premium === 1); + }) + .catch(() => {}); + }; + + useEffect(load, [steamId]); + + const saveBp = async () => { + const res = await fetch(`/api/admin/battlepass/${steamId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ level: editLevel, experience: editXp, has_premium: editPremium }), + }); + const d = await res.json(); + setMsg(d.success ? 'Saved!' : 'Error'); + }; + + const toggleClaim = async (questId: string) => { + await fetch(`/api/admin/battlepass/${steamId}/quests/${questId}/toggle-claim`, { method: 'POST' }); + load(); + }; + + if (!bp) return
Loading...
; + + return ( +
+ ← Back +

Battle Pass

+

Steam ID: {steamId}

+ + {msg &&

{msg}

} + +
+
+

Settings

+
+
+ + setEditLevel(Number(e.target.value))} + className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> +
+
+ + setEditXp(Number(e.target.value))} + className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> +
+
+ + setEditPremium(e.target.checked)} + className="mt-2 block" /> +
+
+ +
+ +
+

Quests ({quests.length})

+
+ {quests.map(q => ( +
+
+
{q.name || q.quest_id}
+
{q.type} — {q.progress}/{q.target}
+
+ + {q.completed ? 'Completed' : 'In Progress'} + + {q.claimed ? ' — Claimed' : ''} +
+
+ {q.completed && !q.claimed && ( + + )} +
+ ))} +
+
+
+
+ ); +} +``` + +- [ ] **Step 3: Create BP admin API routes** + +Create `backend/src/app/api/admin/battlepass/route.ts`: + +```ts +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const bps = db.prepare('SELECT * FROM battle_passes ORDER BY updated_at DESC').all(); + return NextResponse.json(bps); +} +``` + +Create `backend/src/app/api/admin/battlepass/[steamId]/route.ts`: + +```ts +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET(request: NextRequest, { params }: { params: { steamId: string } }) { + const db = getDb(); + const battlePass = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); + const quests = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id').all(params.steamId); + return NextResponse.json({ battlePass, quests }); +} + +export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { + const body = await request.json(); + const db = getDb(); + db.prepare(` + UPDATE battle_passes SET level = ?, experience = ?, has_premium = ?, updated_at = datetime('now') + WHERE steam_id = ? + `).run(body.level, body.experience, body.has_premium ? 1 : 0, params.steamId); + return NextResponse.json({ success: true }); +} +``` + +Create `backend/src/app/api/admin/battlepass/[steamId]/quests/[questId]/toggle-claim/route.ts`: + +```ts +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function POST(request: NextRequest, { params }: { params: { steamId: string; questId: string } }) { + const db = getDb(); + const quest = db.prepare( + 'SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?' + ).get(params.steamId, params.questId) as any; + if (!quest) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + + db.prepare("UPDATE battle_pass_quests SET claimed = 1, updated_at = datetime('now') WHERE id = ?").run(quest.id); + return NextResponse.json({ success: true }); +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add backend/src/app/admin/battlepass/page.tsx backend/src/app/admin/battlepass/\[steamId\]/page.tsx +git add backend/src/app/api/admin/battlepass/route.ts backend/src/app/api/admin/battlepass/\[steamId\]/route.ts +git add backend/src/app/api/admin/battlepass/\[steamId\]/quests/\[questId\]/toggle-claim/route.ts +git commit -m "feat: add admin battle pass pages" +``` + +--- + +### Task 19: Admin matches page + +**Files:** +- Create: `backend/src/app/admin/matches/page.tsx` +- Create: `backend/src/app/api/admin/matches/route.ts` + +- [ ] **Step 1: Create admin matches API route** + +```ts +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET(request: NextRequest) { + const db = getDb(); + const { searchParams } = new URL(request.url); + const hero = searchParams.get('hero'); + const difficulty = searchParams.get('difficulty'); + const limit = parseInt(searchParams.get('limit') || '100'); + const offset = parseInt(searchParams.get('offset') || '0'); + + let query = 'SELECT * FROM game_history WHERE 1=1'; + const params: any[] = []; + + if (hero) { query += ' AND hero LIKE ?'; params.push(`%${hero}%`); } + if (difficulty) { query += ' AND difficulty = ?'; params.push(difficulty); } + + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; + params.push(limit, offset); + + const matches = db.prepare(query).all(...params); + return NextResponse.json(matches); +} +``` + +- [ ] **Step 2: Create matches page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; + +export default function MatchesPage() { + const [matches, setMatches] = useState([]); + const [heroFilter, setHeroFilter] = useState(''); + const [diffFilter, setDiffFilter] = useState(''); + + const load = () => { + const params = new URLSearchParams(); + if (heroFilter) params.set('hero', heroFilter); + if (diffFilter) params.set('difficulty', diffFilter); + fetch(`/api/admin/matches?${params}`) + .then(r => r.json()) + .then(setMatches) + .catch(() => {}); + }; + + useEffect(load, [heroFilter, diffFilter]); + + const difficulties = ['', 'easy', 'normal', 'hard', 'impossible', 'death_sentence']; + + return ( +
+

Match History

+
+ setHeroFilter(e.target.value)} + className="px-3 py-2 bg-gray-800 rounded text-white text-sm" /> + +
+
+ + + + + + + + + + {matches.map(m => ( + + + + + + + + + + ))} + +
Steam IDHeroResultDifficultyK/DDurationDate
{m.steam_id}{m.hero}{m.result}{m.difficulty}{m.kills}/{m.deaths}{Math.floor((m.duration || 0) / 60)}m{m.created_at}
+
+
+ ); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/app/admin/matches/page.tsx backend/src/app/api/admin/matches/route.ts +git commit -m "feat: add admin matches page" +``` + +--- + +### Task 20: Admin promocodes page + +**Files:** +- Create: `backend/src/app/admin/promocodes/page.tsx` +- Create: `backend/src/app/api/admin/promocodes/route.ts` + +- [ ] **Step 1: Create promocodes admin API route** + +```ts +import { NextRequest, NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const codes = db.prepare('SELECT * FROM promo_codes ORDER BY code').all(); + return NextResponse.json(codes); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const db = getDb(); + db.prepare(` + INSERT OR REPLACE INTO promo_codes (code, free_currency, donate_currency, dust_currency, max_uses, expires_at) + VALUES (?, ?, ?, ?, ?, ?) + `).run(body.code?.toUpperCase(), body.free_currency || 0, body.donate_currency || 0, body.dust_currency || 0, + body.max_uses || 1, body.expires_at || null); + return NextResponse.json({ success: true }); +} + +export async function DELETE(request: NextRequest) { + const { searchParams } = new URL(request.url); + const code = searchParams.get('code'); + if (!code) return NextResponse.json({ error: 'code required' }, { status: 400 }); + const db = getDb(); + db.prepare('DELETE FROM promo_codes WHERE code = ?').run(code.toUpperCase()); + return NextResponse.json({ success: true }); +} +``` + +- [ ] **Step 2: Create promocodes page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; + +export default function PromoCodesPage() { + const [codes, setCodes] = useState([]); + const [form, setForm] = useState({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 }); + + const load = () => { fetch('/api/admin/promocodes').then(r => r.json()).then(setCodes); }; + useEffect(load, []); + + const create = async () => { + await fetch('/api/admin/promocodes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + setForm({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 }); + load(); + }; + + const del = async (code: string) => { + await fetch(`/api/admin/promocodes?code=${code}`, { method: 'DELETE' }); + load(); + }; + + return ( +
+

Promo Codes

+ +
+

Create Code

+
+ setForm({...form, code: e.target.value})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, free_currency: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, donate_currency: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, dust_currency: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> + setForm({...form, max_uses: Number(e.target.value)})} + className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> +
+ +
+ +
+ + + + + + + + + + {codes.map(c => ( + + + + + + + + + + ))} + +
CodeFreeDonateDustUsesExpires
{c.code}{c.free_currency}{c.donate_currency}{c.dust_currency}{c.current_uses}/{c.max_uses}{c.expires_at || 'Never'}
+
+
+ ); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/app/admin/promocodes/page.tsx backend/src/app/api/admin/promocodes/route.ts +git commit -m "feat: add admin promocodes page" +``` + +--- + +### Task 21: Admin store page + +**Files:** +- Create: `backend/src/app/admin/store/page.tsx` +- Create: `backend/src/app/api/admin/store/route.ts` + +- [ ] **Step 1: Create store admin API route** + +```ts +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const purchases = db.prepare(` + SELECT p.*, pl.player_name FROM purchases p + LEFT JOIN players pl ON p.steam_id = pl.steam_id + ORDER BY p.created_at DESC LIMIT 200 + `).all(); + const effects = db.prepare('SELECT * FROM active_effects').all(); + return NextResponse.json({ purchases, effects }); +} +``` + +- [ ] **Step 2: Create store page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; + +export default function StorePage() { + const [purchases, setPurchases] = useState([]); + const [effects, setEffects] = useState([]); + + useEffect(() => { + fetch('/api/admin/store').then(r => r.json()).then(d => { + setPurchases(d.purchases || []); + setEffects(d.effects || []); + }); + }, []); + + return ( +
+
+

Store Purchases

+
+ + + + + + + + + {purchases.map(p => ( + + + + + + + + ))} + +
PlayerItemCategoryCostDate
{p.player_name || p.steam_id}{p.item_id}{p.item_category}{p.price_free || p.price_donate || p.price_dust || 0}{p.created_at}
+
+
+ +
+

Active Effects

+
+ + + + + + + + {effects.map((e: any) => ( + + + + + ))} + +
Steam IDEffects
{e.steam_id}{e.effects}
+
+
+
+ ); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/app/admin/store/page.tsx backend/src/app/api/admin/store/route.ts +git commit -m "feat: add admin store page" +``` + +--- + +### Task 22: Admin contracts page + +**Files:** +- Create: `backend/src/app/admin/contracts/page.tsx` +- Create: `backend/src/app/api/admin/contracts/route.ts` + +- [ ] **Step 1: Create admin contracts API** + +```ts +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const contracts = db.prepare('SELECT * FROM death_sentence_contracts').all(); + return NextResponse.json(contracts); +} +``` + +- [ ] **Step 2: Create contracts page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; + +export default function ContractsPage() { + const [contracts, setContracts] = useState([]); + + useEffect(() => { + fetch('/api/admin/contracts').then(r => r.json()).then(setContracts); + }, []); + + return ( +
+

Death Sentence Contracts

+
+ + + + + + + + {contracts.map((c: any) => ( + + + + + + ))} + +
Steam IDContract DataUpdated
{c.steam_id}{c.contracts}{c.updated_at}
+
+
+ ); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/app/admin/contracts/page.tsx backend/src/app/api/admin/contracts/route.ts +git commit -m "feat: add admin contracts page" +``` + +--- + +### Task 23: Admin arsenal page + +**Files:** +- Create: `backend/src/app/admin/arsenal/page.tsx` +- Create: `backend/src/app/api/admin/arsenal/route.ts` + +- [ ] **Step 1: Create admin arsenal API** + +```ts +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +export async function GET() { + const db = getDb(); + const inventory = db.prepare('SELECT * FROM arsenal_inventory ORDER BY steam_id').all(); + const loadouts = db.prepare('SELECT * FROM arsenal_loadouts ORDER BY steam_id').all(); + const listings = db.prepare("SELECT * FROM arsenal_market_listings WHERE status = 'active' ORDER BY created_at DESC").all(); + return NextResponse.json({ inventory, loadouts, listings }); +} +``` + +- [ ] **Step 2: Create arsenal page** + +```tsx +'use client'; +import { useEffect, useState } from 'react'; + +export default function ArsenalPage() { + const [data, setData] = useState({}); + + useEffect(() => { + fetch('/api/admin/arsenal').then(r => r.json()).then(setData); + }, []); + + return ( +
+

Arsenal

+
+
+

Inventory ({data.inventory?.length || 0})

+
+ {data.inventory?.map((i: any, idx: number) => ( +
+ {i.item_name} + [{i.quality}] +
{i.steam_id}
+
+ ))} +
+
+ +
+

Loadouts ({data.loadouts?.length || 0})

+
+ {data.loadouts?.map((l: any, idx: number) => ( +
+ {l.steam_id} + {l.hero_name} +
{l.loadout}
+
+ ))} +
+
+ +
+

Active Listings ({data.listings?.length || 0})

+
+ {data.listings?.map((l: any, idx: number) => ( +
+ {l.item_name} + {l.price_free} free +
{l.steam_id}
+
+ ))} +
+
+
+
+ ); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/app/admin/arsenal/page.tsx backend/src/app/api/admin/arsenal/route.ts +git commit -m "feat: add admin arsenal page" +``` + +--- + +### Task 24: Docker setup + +**Files:** +- Create: `backend/Dockerfile` +- Create: `backend/docker-compose.yml` +- Create: `backend/.dockerignore` +- Create: `backend/docker-entrypoint.sh` + +- [ ] **Step 1: Create .dockerignore** + +``` +node_modules +.next +.git +data +README.md +``` + +- [ ] **Step 2: Create Dockerfile** + +```dockerfile +FROM node:20-alpine AS base + +# Stage 1: Install deps +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci + +# Stage 2: Build +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +# Stage 3: Production runner +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV DB_PATH=/app/data/zombie_invasion.db + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy standalone build +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public +COPY --from=builder /app/docker-entrypoint.sh ./ +COPY --from=builder /app/data ./data + +RUN chmod +x docker-entrypoint.sh + +RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +ENTRYPOINT ["/bin/sh", "docker-entrypoint.sh"] +``` + +- [ ] **Step 3: Create docker-entrypoint.sh** + +```bash +#!/bin/sh +set -e + +# Ensure data directory exists +mkdir -p /app/data + +# Start the application +exec node server.js +``` + +- [ ] **Step 4: Make entrypoint executable** + +Run: `chmod +x /Users/achmad/Documents/dota/3728427109/backend/docker-entrypoint.sh` + +- [ ] **Step 5: Create docker-compose.yml** + +```yaml +version: '3.8' + +services: + app: + build: . + ports: + - "6100:3000" # Host:6100 → Container:3000 + volumes: + - ./data:/app/data # Persist SQLite database + environment: + - ADMIN_PASSWORD=admin123 + - NODE_ENV=production + restart: unless-stopped +``` + +- [ ] **Step 6: Build and test Docker image** + +Run: `cd /Users/achmad/Documents/dota/3728427109/backend && docker compose build` +Expected: Build succeeds + +Run: `docker compose up -d` +Expected: Container starts, accessible at http://localhost:6100 + +Run: `curl http://localhost:6100/api/admin/check` +Expected: `{"authenticated":false}` + +- [ ] **Step 7: Commit** + +```bash +git add backend/Dockerfile backend/docker-compose.yml backend/.dockerignore backend/docker-entrypoint.sh +git commit -m "feat: add Docker setup with multi-stage build" +``` + +--- + +### Task 25: Update postman_collection.json + +**Files:** +- Modify: `postman_collection.json` + +- [ ] **Step 1: Update the base_url variable** + +Change the `base_url` variable value from `http://82.146.52.69:3000/api` to `http://localhost:6100/api` for local dev. Since the user will host on their domain later, use `{{base_url}}` properly. + +In the `variable` array at the bottom of postman_collection.json, change: + +```json +{ + "key": "base_url", + "value": "http://localhost:6100/api", + "type": "string" +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add postman_collection.json +git commit -m "chore: update postman base_url to localhost:6100" +```