add stats view
This commit is contained in:
385
package-lock.json
generated
385
package-lock.json
generated
@@ -9,13 +9,13 @@
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -26,13 +26,13 @@
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -43,13 +43,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -60,13 +60,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -77,13 +77,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -94,13 +94,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -111,13 +111,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -128,13 +128,13 @@
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -145,13 +145,13 @@
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -162,13 +162,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -179,13 +179,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -196,13 +196,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -213,13 +213,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -230,13 +230,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -247,13 +247,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -264,13 +264,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -281,13 +281,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -298,13 +298,30 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -315,13 +332,30 @@
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -332,13 +366,30 @@
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -349,13 +400,13 @@
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -366,13 +417,13 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -383,13 +434,13 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -400,7 +451,7 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
@@ -761,9 +812,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -771,32 +822,53 @@
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
@@ -840,6 +912,19 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
@@ -924,6 +1009,23 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -939,21 +1041,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
@@ -962,19 +1067,25 @@
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -995,6 +1106,12 @@
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
182
src/lib/game.ts
182
src/lib/game.ts
@@ -1,8 +1,32 @@
|
||||
import { getTypeInfo, getEffectiveness } from './types';
|
||||
import { getTypeInfo, getEffectiveness, TypeName, TYPES } from './types';
|
||||
import { Question, Sampler } from './sampler';
|
||||
|
||||
const STORAGE_KEY = 'pokemon-type-quiz-data';
|
||||
|
||||
export interface CombinationStats {
|
||||
key: string;
|
||||
attackType: TypeName;
|
||||
defenderTypes: TypeName[];
|
||||
total: number;
|
||||
correct: number;
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
export interface TypeChartStats {
|
||||
attackType: TypeName;
|
||||
total: number;
|
||||
correct: number;
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
export interface FullTypeChartCell {
|
||||
attackType: TypeName;
|
||||
defenderTypes: TypeName[];
|
||||
total: number;
|
||||
correct: number;
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
currentQuestion: Question | null;
|
||||
selectedAnswer: number | null;
|
||||
@@ -184,4 +208,160 @@ export class Game {
|
||||
|
||||
return explanation;
|
||||
}
|
||||
|
||||
getCombinationStats(): CombinationStats[] {
|
||||
const history = this.sampler.getHistory();
|
||||
const comboMap = new Map<string, { total: number; correct: number }>();
|
||||
|
||||
for (const record of history) {
|
||||
const existing = comboMap.get(record.key) || { total: 0, correct: 0 };
|
||||
existing.total++;
|
||||
if (record.correct) existing.correct++;
|
||||
comboMap.set(record.key, existing);
|
||||
}
|
||||
|
||||
const stats: CombinationStats[] = [];
|
||||
for (const [key, data] of comboMap) {
|
||||
const [attackType, ...defTypes] = key.split(':');
|
||||
const defenderTypes = defTypes.join(':').split('/') as TypeName[];
|
||||
stats.push({
|
||||
key,
|
||||
attackType: attackType as TypeName,
|
||||
defenderTypes,
|
||||
total: data.total,
|
||||
correct: data.correct,
|
||||
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
return stats.sort((a, b) => b.accuracy - a.accuracy);
|
||||
}
|
||||
|
||||
getBestCombinations(limit: number = 5): CombinationStats[] {
|
||||
const stats = this.getCombinationStats().filter(s => s.total >= 3);
|
||||
return stats.slice(0, limit);
|
||||
}
|
||||
|
||||
getWorstCombinations(limit: number = 5): CombinationStats[] {
|
||||
const stats = this.getCombinationStats().filter(s => s.total >= 3);
|
||||
return stats.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
getTypeChartStats(): TypeChartStats[] {
|
||||
const history = this.sampler.getHistory();
|
||||
const typeMap = new Map<TypeName, { total: number; correct: number }>();
|
||||
|
||||
for (const type of TYPES) {
|
||||
typeMap.set(type.name, { total: 0, correct: 0 });
|
||||
}
|
||||
|
||||
for (const record of history) {
|
||||
const [attackType] = record.key.split(':');
|
||||
const data = typeMap.get(attackType as TypeName);
|
||||
if (data) {
|
||||
data.total++;
|
||||
if (record.correct) data.correct++;
|
||||
}
|
||||
}
|
||||
|
||||
const stats: TypeChartStats[] = [];
|
||||
for (const type of TYPES) {
|
||||
const data = typeMap.get(type.name)!;
|
||||
stats.push({
|
||||
attackType: type.name,
|
||||
total: data.total,
|
||||
correct: data.correct,
|
||||
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
return stats.sort((a, b) => b.accuracy - a.accuracy);
|
||||
}
|
||||
|
||||
getDefendingTypeStats(): TypeChartStats[] {
|
||||
const history = this.sampler.getHistory();
|
||||
const typeMap = new Map<TypeName, { total: number; correct: number }>();
|
||||
|
||||
for (const type of TYPES) {
|
||||
typeMap.set(type.name, { total: 0, correct: 0 });
|
||||
}
|
||||
|
||||
for (const record of history) {
|
||||
// Parse defender types from key format: "AttackType:DefType1/DefType2"
|
||||
const parts = record.key.split(':');
|
||||
const defenderPart = parts[1];
|
||||
const defenderTypes = defenderPart.split('/');
|
||||
|
||||
// Track each defender type
|
||||
for (const defType of defenderTypes) {
|
||||
const data = typeMap.get(defType as TypeName);
|
||||
if (data) {
|
||||
data.total++;
|
||||
if (record.correct) data.correct++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stats: TypeChartStats[] = [];
|
||||
for (const type of TYPES) {
|
||||
const data = typeMap.get(type.name)!;
|
||||
stats.push({
|
||||
attackType: type.name,
|
||||
total: data.total,
|
||||
correct: data.correct,
|
||||
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
return stats.sort((a, b) => b.accuracy - a.accuracy);
|
||||
}
|
||||
|
||||
private getAllDefenderTypes(): TypeName[][] {
|
||||
const result: TypeName[][] = [];
|
||||
// Single types
|
||||
for (let i = 0; i < TYPES.length; i++) {
|
||||
result.push([TYPES[i].name]);
|
||||
}
|
||||
// Dual types
|
||||
for (let i = 0; i < TYPES.length; i++) {
|
||||
for (let j = i + 1; j < TYPES.length; j++) {
|
||||
result.push([TYPES[i].name, TYPES[j].name]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getFullTypeChartStats(): FullTypeChartCell[] {
|
||||
const history = this.sampler.getHistory();
|
||||
const defenderTypesList = this.getAllDefenderTypes();
|
||||
|
||||
// Build a map for quick lookup
|
||||
const statsMap = new Map<string, { total: number; correct: number }>();
|
||||
|
||||
for (const record of history) {
|
||||
const existing = statsMap.get(record.key) || { total: 0, correct: 0 };
|
||||
existing.total++;
|
||||
if (record.correct) existing.correct++;
|
||||
statsMap.set(record.key, existing);
|
||||
}
|
||||
|
||||
const cells: FullTypeChartCell[] = [];
|
||||
|
||||
for (const attackType of TYPES) {
|
||||
for (const defenderTypes of defenderTypesList) {
|
||||
const key = `${attackType.name}:${defenderTypes.sort().join('/')}`;
|
||||
const data = statsMap.get(key) || { total: 0, correct: 0 };
|
||||
|
||||
cells.push({
|
||||
attackType: attackType.name,
|
||||
defenderTypes,
|
||||
total: data.total,
|
||||
correct: data.correct,
|
||||
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
}
|
||||
|
||||
221
src/main.ts
221
src/main.ts
@@ -1,9 +1,11 @@
|
||||
import { Game } from './lib/game';
|
||||
import { Game, CombinationStats, TypeChartStats, FullTypeChartCell } from './lib/game';
|
||||
import { getTypeInfo, TypeName } from './lib/types';
|
||||
import './style.css';
|
||||
|
||||
const app = document.getElementById('app')!;
|
||||
|
||||
let currentView: 'quiz' | 'stats' = 'quiz';
|
||||
let typeChartView: 'attacking' | 'defending' = 'attacking';
|
||||
const game = new Game(render);
|
||||
game.nextQuestion();
|
||||
|
||||
@@ -14,6 +16,11 @@ function renderTypeBadge(name: TypeName, size: 'small' | 'large' = 'small'): str
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
if (currentView === 'stats') {
|
||||
renderStatsPage();
|
||||
return;
|
||||
}
|
||||
|
||||
const state = game.getState();
|
||||
const { options, startKey } = game.getOptions();
|
||||
const stats = game.getStats();
|
||||
@@ -70,6 +77,7 @@ function render(): void {
|
||||
<span class="stat">Streak: ${state.streak}</span>
|
||||
<span class="stat">Accuracy: ${stats.accuracy.toFixed(1)}%</span>
|
||||
<span class="stat">Total: ${stats.total}</span>
|
||||
<button class="stats-btn" id="statsBtn">View Stats</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -105,6 +113,209 @@ function render(): void {
|
||||
attachEventListeners();
|
||||
}
|
||||
|
||||
function renderCombinationItem(combo: CombinationStats): string {
|
||||
const defenderHtml = combo.defenderTypes.length === 1
|
||||
? renderTypeBadge(combo.defenderTypes[0], 'small')
|
||||
: `${renderTypeBadge(combo.defenderTypes[0], 'small')} / ${renderTypeBadge(combo.defenderTypes[1], 'small')}`;
|
||||
|
||||
const accuracyClass = combo.accuracy >= 80 ? 'high' : combo.accuracy >= 50 ? 'medium' : 'low';
|
||||
|
||||
return `
|
||||
<div class="combo-item">
|
||||
<div class="combo-types">
|
||||
<span class="combo-attack">${renderTypeBadge(combo.attackType, 'small')}</span>
|
||||
<span class="combo-vs">vs</span>
|
||||
<span class="combo-defend">${defenderHtml}</span>
|
||||
</div>
|
||||
<div class="combo-stats">
|
||||
<span class="combo-accuracy ${accuracyClass}">${combo.accuracy.toFixed(0)}%</span>
|
||||
<span class="combo-count">(${combo.correct}/${combo.total})</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTypeChartCell(typeStat: TypeChartStats): string {
|
||||
const info = getTypeInfo(typeStat.attackType);
|
||||
const accuracyClass = typeStat.total === 0 ? 'no-data' : typeStat.accuracy >= 80 ? 'high' : typeStat.accuracy >= 50 ? 'medium' : 'low';
|
||||
const display = typeStat.total === 0 ? '-' : `${typeStat.accuracy.toFixed(0)}%`;
|
||||
|
||||
return `
|
||||
<div class="type-cell" style="background: ${info.backgroundColor}; color: ${info.color};" title="${typeStat.attackType}: ${typeStat.correct}/${typeStat.total}">
|
||||
<span class="type-cell-name">${typeStat.attackType.slice(0, 3)}</span>
|
||||
<span class="type-cell-accuracy ${accuracyClass}">${display}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFullTypeChart(cells: FullTypeChartCell[]): string {
|
||||
const defenderTypes = ['Normal', 'Fire', 'Water', 'Electric', 'Grass', 'Ice', 'Fighting', 'Poison', 'Ground', 'Flying', 'Psychic', 'Bug', 'Rock', 'Ghost', 'Dragon', 'Dark', 'Steel', 'Fairy'];
|
||||
|
||||
// Create a map for quick cell lookup
|
||||
const cellMap = new Map<string, FullTypeChartCell>();
|
||||
for (const cell of cells) {
|
||||
const defenderKey = cell.defenderTypes.join('/');
|
||||
const key = `${cell.attackType}:${defenderKey}`;
|
||||
cellMap.set(key, cell);
|
||||
}
|
||||
|
||||
// Generate header row
|
||||
const headerCells = defenderTypes.map(def => {
|
||||
const info = getTypeInfo(def as TypeName);
|
||||
return `<div class="full-chart-header" style="background: ${info.backgroundColor}; color: ${info.color};">
|
||||
${def.slice(0, 3)}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Generate rows
|
||||
const rows = defenderTypes.map(rowType => {
|
||||
const rowInfo = getTypeInfo(rowType as TypeName);
|
||||
const dataCells = defenderTypes.map(colType => {
|
||||
const cell = cellMap.get(`${rowType}:${colType}`);
|
||||
const total = cell?.total || 0;
|
||||
const accuracy = cell?.accuracy || 0;
|
||||
|
||||
let bgColor = 'var(--bg-accent)';
|
||||
let textColor = 'var(--text-secondary)';
|
||||
let display = '-';
|
||||
|
||||
if (total > 0) {
|
||||
display = `${accuracy.toFixed(0)}`;
|
||||
if (accuracy >= 80) {
|
||||
bgColor = 'rgba(74, 222, 128, 0.3)';
|
||||
textColor = 'var(--success)';
|
||||
} else if (accuracy >= 50) {
|
||||
bgColor = 'rgba(251, 191, 36, 0.3)';
|
||||
textColor = '#fbbf24';
|
||||
} else {
|
||||
bgColor = 'rgba(248, 113, 113, 0.3)';
|
||||
textColor = 'var(--error)';
|
||||
}
|
||||
}
|
||||
|
||||
return `<div class="full-chart-cell" style="background: ${bgColor}; color: ${textColor};" title="${rowType} vs ${colType}: ${cell?.correct || 0}/${total}">
|
||||
${display}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="full-chart-row">
|
||||
<div class="full-chart-row-header" style="background: ${rowInfo.backgroundColor}; color: ${rowInfo.color};">
|
||||
${rowType.slice(0, 3)}
|
||||
</div>
|
||||
${dataCells}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="full-type-chart">
|
||||
<div class="full-chart-header-row">
|
||||
<div class="full-chart-corner"></div>
|
||||
${headerCells}
|
||||
</div>
|
||||
${rows}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderStatsPage(): void {
|
||||
const stats = game.getStats();
|
||||
const bestCombos = game.getBestCombinations(5);
|
||||
const worstCombos = game.getWorstCombinations(5);
|
||||
const typeChartStats = typeChartView === 'attacking' ? game.getTypeChartStats() : game.getDefendingTypeStats();
|
||||
const fullTypeChartCells = game.getFullTypeChartStats();
|
||||
|
||||
const hasData = stats.total > 0;
|
||||
|
||||
const bestCombosHtml = bestCombos.length > 0
|
||||
? bestCombos.map(renderCombinationItem).join('')
|
||||
: '<p class="no-data-text">Answer at least 3 questions to see stats</p>';
|
||||
|
||||
const worstCombosHtml = worstCombos.length > 0
|
||||
? worstCombos.map(renderCombinationItem).join('')
|
||||
: '<p class="no-data-text">Answer at least 3 questions to see stats</p>';
|
||||
|
||||
const typeChartHtml = typeChartStats.map(renderTypeChartCell).join('');
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="stats-container">
|
||||
<header class="stats-header">
|
||||
<button class="back-btn" id="backBtn">← Back to Quiz</button>
|
||||
<h1>Your Statistics</h1>
|
||||
</header>
|
||||
|
||||
<div class="stats-overview">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${stats.total}</div>
|
||||
<div class="stat-label">Total Questions</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${stats.accuracy.toFixed(1)}%</div>
|
||||
<div class="stat-label">Accuracy</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${stats.correct}</div>
|
||||
<div class="stat-label">Correct</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${hasData ? `
|
||||
<section class="stats-section">
|
||||
<h2>Best Combinations</h2>
|
||||
<p class="section-subtitle">Highest accuracy (min. 3 attempts)</p>
|
||||
<div class="combo-list">
|
||||
${bestCombosHtml}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-section">
|
||||
<h2>Needs Practice</h2>
|
||||
<p class="section-subtitle">Lowest accuracy (min. 3 attempts)</p>
|
||||
<div class="combo-list">
|
||||
${worstCombosHtml}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-section">
|
||||
<h2>Type Chart Performance</h2>
|
||||
<div class="chart-toggle-row">
|
||||
<button class="chart-toggle ${typeChartView === 'attacking' ? 'active' : ''}" data-view="attacking">Attacking</button>
|
||||
<button class="chart-toggle ${typeChartView === 'defending' ? 'active' : ''}" data-view="defending">Defending</button>
|
||||
</div>
|
||||
<p class="section-subtitle">Accuracy by ${typeChartView} type</p>
|
||||
<div class="type-chart">
|
||||
${typeChartHtml}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-section full-chart-section">
|
||||
<h2>Full Type Chart</h2>
|
||||
<p class="section-subtitle">Accuracy for each type combination</p>
|
||||
${renderFullTypeChart(fullTypeChartCells)}
|
||||
</section>
|
||||
` : `
|
||||
<div class="no-data-message">
|
||||
<p>No data yet. Start playing to track your statistics!</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('backBtn')?.addEventListener('click', () => {
|
||||
currentView = 'quiz';
|
||||
render();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.chart-toggle').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const view = btn.getAttribute('data-view') as 'attacking' | 'defending';
|
||||
typeChartView = view;
|
||||
render();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function attachEventListeners(): void {
|
||||
document.querySelectorAll('.option-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -126,6 +337,14 @@ function attachEventListeners(): void {
|
||||
game.nextQuestion();
|
||||
});
|
||||
}
|
||||
|
||||
const statsBtn = document.getElementById('statsBtn');
|
||||
if (statsBtn) {
|
||||
statsBtn.addEventListener('click', () => {
|
||||
currentView = 'stats';
|
||||
render();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
|
||||
334
src/style.css
334
src/style.css
@@ -309,6 +309,340 @@ body {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Stats Button */
|
||||
.stats-btn {
|
||||
padding: 6px 14px;
|
||||
background: var(--bg-accent);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.stats-btn:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Stats Page */
|
||||
.stats-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stats-header h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--accent), #ff6b6b);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
border-color: var(--accent);
|
||||
background: rgba(233, 69, 96, 0.1);
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.stats-section h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.combo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.combo-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-accent);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.combo-types {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.combo-vs {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.combo-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.combo-accuracy {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.combo-accuracy.high {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.combo-accuracy.medium {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.combo-accuracy.low {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.combo-count {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Type Chart */
|
||||
.type-chart {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.type-cell {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.type-cell:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.type-cell-name {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.type-cell-accuracy {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
margin-top: 2px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.type-cell-accuracy.high {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.type-cell-accuracy.medium {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.type-cell-accuracy.low {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.type-cell-accuracy.no-data {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.no-data-message p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Full Type Chart */
|
||||
.full-chart-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-toggle-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-toggle {
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-accent);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.chart-toggle:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.chart-toggle.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.full-type-chart {
|
||||
display: grid;
|
||||
grid-template-columns: 50px repeat(18, 1fr);
|
||||
gap: 2px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.full-chart-header-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.full-chart-corner {
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-accent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.full-chart-header {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.full-chart-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.full-chart-row-header {
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.full-chart-cell {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
border-radius: 3px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.full-chart-cell:hover {
|
||||
transform: scale(1.15);
|
||||
z-index: 1;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 12px;
|
||||
|
||||
Reference in New Issue
Block a user