diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 71dfc46..f37762e 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -24,29 +24,57 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - workspaces: parser + workspaces: | + 2016/tools/xlsxread + 2017/tools/xlsxread + + - uses: pnpm/action-setup@v4 + with: + package_json_file: 2017/package.json - uses: actions/setup-node@v4 with: node-version: '24' - cache: 'npm' - cache-dependency-path: package-lock.json + cache: 'pnpm' + cache-dependency-path: | + 2016/pnpm-lock.yaml + 2017/pnpm-lock.yaml - - run: npm ci - - # One parser binary builds every dataset; build-db.js reads the dataset - # list from src/datasets.js and gzips each database in place, leaving no - # uncompressed file behind. - - name: Build databases + - name: Build 2016 site + working-directory: '2016' run: | - npm run build:rust - npm run build:db + set -euo pipefail + cargo build --release --manifest-path tools/xlsxread/Cargo.toml + ./tools/xlsxread/target/release/xlsxread build \ + --schema tools/xlsxread/configs/thptqg2016-data.toml \ + --input data --output public/thptqg2016.db + pnpm install --frozen-lockfile + gzip -k -9 public/thptqg2016.db + pnpm build + rm -f dist/thptqg2016.db - # One Vite build produces every page. scripts/assemble-site.js copies the - # emitted index.html to each dataset path (and the legacy nested URLs), - # then fails the job if any uncompressed database reached the artifact. - - name: Build and assemble site - run: npm run build:site + - name: Build 2017 site variants + working-directory: '2017' + run: | + set -euo pipefail + cargo build --release --manifest-path tools/xlsxread/Cargo.toml + ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data.toml --input data --output public/thptqg2017.db + ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old.toml --input data-old --output public-old/thptqg2017.db + ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old2.toml --input data-old2 --output public-old2/thptqg2017.db + pnpm install --frozen-lockfile + gzip -kf -9 public/thptqg2017.db + gzip -kf -9 public-old/thptqg2017.db + gzip -kf -9 public-old2/thptqg2017.db + pnpm build:all + rm -f dist/thptqg2017.db dist/old/thptqg2017.db dist/old2/thptqg2017.db + + - name: Assemble site + run: | + set -euo pipefail + mkdir -p _site + cp index.html _site/ + cp -r 2016/dist _site/2016 + cp -r 2017/dist _site/2017 - uses: actions/upload-pages-artifact@v3 with: diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c67537c..0000000 --- a/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -### Editors ### -.idea -*.iws -*.iml -*.ipr -.vscode/ - -### Mac OS ### -.DS_Store - -### Node ### -node_modules/ - -### Build output ### -dist/ - -### Generated databases and Vite publicDir staging ### -.build/ - -### Rust build artefacts ### -parser/target/ - -### Assembled Pages artifact ### -_site/ diff --git a/2016/.gitignore b/2016/.gitignore new file mode 100644 index 0000000..051b283 --- /dev/null +++ b/2016/.gitignore @@ -0,0 +1,146 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite logs files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Generated database files +public/*.db +public/*.db.gz + +# Rust build artifacts +tools/xlsxread/target/ diff --git a/LICENSE b/2016/LICENSE similarity index 100% rename from LICENSE rename to 2016/LICENSE diff --git a/2016/README.md b/2016/README.md new file mode 100644 index 0000000..a086341 --- /dev/null +++ b/2016/README.md @@ -0,0 +1,57 @@ +# thptqg2016 + +Lookup tool for Vietnam's 2016 National High School Graduation Exam (THPT Quốc gia) scores — 877,461 candidates nationwide. + +Fully static app running entirely in the browser (SQLite via `sql.js`). No backend, no query logging. + +## Features + +- **Quick lookup** by exam ID (`số báo danh`) or full name (diacritics-insensitive) +- **Custom read-only SQL** (SELECT / PRAGMA / EXPLAIN / WITH) with 7 built-in preset queries +- **Complete data**: 12 subjects (Math, Literature, Physics, Chemistry, Biology, History, Geography, English, French, German, Japanese, Chinese), exam cluster, date of birth, gender +- Safety caps: 100 rows for lookup, 1000 rows for custom SQL +- Dark mode, `Ctrl+Enter` shortcut to run queries + +## Demo + + + +## Development + +```bash +# Build the SQLite database from source Excel files (requires Rust stable) +pnpm run build:db + +# Or build in two steps: +pnpm run build:rust # compile the xlsxread binary +./tools/xlsxread/target/release/xlsxread build \ + --schema tools/xlsxread/configs/thptqg2016-data.toml \ + --input data \ + --output public/thptqg2016.db + +pnpm run dev # Vite dev server +pnpm run build # Production bundle → dist/ +pnpm run lint # ESLint +``` + +The database is built by the `xlsxread` Rust binary (`tools/xlsxread/`), which +reads the 119 mixed `.xls`/`.xlsx` files and auto-detects the column layout per +file (`separate-scores`, `mapped`, or positional default). No Node.js Excel +library is required at build time. + +The GitHub Actions workflow (`.github/workflows/deploy.yml`) compiles +`xlsxread`, builds the DB, gzips it, and deploys to GitHub Pages on every push +to `main`. + +## Tech stack + +React 19 · Vite · sql.js (WASM) · xlsxread (Rust, build-time) · GitHub Pages + +## Documentation + +- [Project overview (PDR)](./docs/project-overview-pdr.md) +- [Codebase summary](./docs/codebase-summary.md) +- [System architecture](./docs/system-architecture.md) +- [Deployment guide](./docs/deployment-guide.md) + +**Source**: Originally published at (currently inaccessible). Data is for reference only. diff --git a/data/2016/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx b/2016/data/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx similarity index 100% rename from data/2016/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx rename to 2016/data/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx diff --git a/data/2016/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx b/2016/data/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx similarity index 100% rename from data/2016/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx rename to 2016/data/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx diff --git a/data/2016/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx b/2016/data/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx similarity index 100% rename from data/2016/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx rename to 2016/data/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx diff --git a/data/2016/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx b/2016/data/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx similarity index 100% rename from data/2016/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx rename to 2016/data/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx diff --git a/data/2016/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls b/2016/data/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls similarity index 100% rename from data/2016/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls rename to 2016/data/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls diff --git a/data/2016/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx b/2016/data/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx similarity index 100% rename from data/2016/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx rename to 2016/data/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx diff --git a/data/2016/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx b/2016/data/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx similarity index 100% rename from data/2016/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx rename to 2016/data/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx diff --git a/data/2016/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx b/2016/data/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx similarity index 100% rename from data/2016/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx rename to 2016/data/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx diff --git a/data/2016/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx b/2016/data/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx similarity index 100% rename from data/2016/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx rename to 2016/data/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx diff --git a/data/2016/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx b/2016/data/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx similarity index 100% rename from data/2016/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx rename to 2016/data/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx diff --git a/data/2016/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx b/2016/data/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx similarity index 100% rename from data/2016/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx rename to 2016/data/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx diff --git a/data/2016/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx b/2016/data/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx similarity index 100% rename from data/2016/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx rename to 2016/data/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx diff --git a/data/2016/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx b/2016/data/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx similarity index 100% rename from data/2016/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx rename to 2016/data/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx diff --git a/data/2016/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx b/2016/data/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx similarity index 100% rename from data/2016/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx rename to 2016/data/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx diff --git a/data/2016/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx b/2016/data/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx similarity index 100% rename from data/2016/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx rename to 2016/data/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx diff --git a/data/2016/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx b/2016/data/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx similarity index 100% rename from data/2016/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx rename to 2016/data/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx diff --git a/data/2016/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx b/2016/data/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx similarity index 100% rename from data/2016/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx rename to 2016/data/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx diff --git a/data/2016/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx b/2016/data/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx similarity index 100% rename from data/2016/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx rename to 2016/data/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx diff --git a/data/2016/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx b/2016/data/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx similarity index 100% rename from data/2016/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx rename to 2016/data/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx diff --git a/data/2016/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx b/2016/data/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx similarity index 100% rename from data/2016/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx rename to 2016/data/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx diff --git a/data/2016/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx b/2016/data/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx similarity index 100% rename from data/2016/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx rename to 2016/data/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx diff --git a/data/2016/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx b/2016/data/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx similarity index 100% rename from data/2016/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx rename to 2016/data/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx diff --git a/data/2016/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx b/2016/data/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx similarity index 100% rename from data/2016/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx rename to 2016/data/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx diff --git a/data/2016/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx b/2016/data/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx similarity index 100% rename from data/2016/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx rename to 2016/data/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx diff --git a/data/2016/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx b/2016/data/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx similarity index 100% rename from data/2016/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx rename to 2016/data/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx diff --git a/data/2016/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx b/2016/data/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx similarity index 100% rename from data/2016/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx rename to 2016/data/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx diff --git a/data/2016/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx b/2016/data/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx similarity index 100% rename from data/2016/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx rename to 2016/data/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx diff --git a/data/2016/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx b/2016/data/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx similarity index 100% rename from data/2016/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx rename to 2016/data/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx diff --git a/data/2016/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx b/2016/data/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx similarity index 100% rename from data/2016/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx rename to 2016/data/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx diff --git a/data/2016/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx b/2016/data/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx similarity index 100% rename from data/2016/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx rename to 2016/data/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx diff --git a/data/2016/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx b/2016/data/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx similarity index 100% rename from data/2016/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx rename to 2016/data/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx diff --git a/data/2016/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx b/2016/data/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx similarity index 100% rename from data/2016/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx rename to 2016/data/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx diff --git a/data/2016/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx b/2016/data/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx similarity index 100% rename from data/2016/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx rename to 2016/data/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx diff --git a/data/2016/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx b/2016/data/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx similarity index 100% rename from data/2016/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx rename to 2016/data/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx diff --git a/data/2016/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx b/2016/data/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx similarity index 100% rename from data/2016/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx rename to 2016/data/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx diff --git a/data/2016/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx b/2016/data/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx similarity index 100% rename from data/2016/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx rename to 2016/data/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx diff --git a/data/2016/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx b/2016/data/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx similarity index 100% rename from data/2016/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx rename to 2016/data/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx diff --git a/data/2016/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx b/2016/data/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx similarity index 100% rename from data/2016/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx rename to 2016/data/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx diff --git a/data/2016/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx b/2016/data/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx similarity index 100% rename from data/2016/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx rename to 2016/data/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx diff --git a/data/2016/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx b/2016/data/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx similarity index 100% rename from data/2016/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx rename to 2016/data/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx diff --git a/data/2016/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx b/2016/data/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx similarity index 100% rename from data/2016/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx rename to 2016/data/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx diff --git a/data/2016/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls b/2016/data/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls similarity index 100% rename from data/2016/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls rename to 2016/data/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls diff --git a/data/2016/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx b/2016/data/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx similarity index 100% rename from data/2016/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx rename to 2016/data/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx diff --git a/data/2016/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx b/2016/data/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx similarity index 100% rename from data/2016/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx rename to 2016/data/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx diff --git a/data/2016/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx b/2016/data/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx similarity index 100% rename from data/2016/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx rename to 2016/data/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx diff --git a/data/2016/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx b/2016/data/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx similarity index 100% rename from data/2016/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx rename to 2016/data/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx diff --git a/data/2016/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx b/2016/data/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx similarity index 100% rename from data/2016/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx rename to 2016/data/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx diff --git a/data/2016/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx b/2016/data/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx similarity index 100% rename from data/2016/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx rename to 2016/data/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx diff --git a/data/2016/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx b/2016/data/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx similarity index 100% rename from data/2016/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx rename to 2016/data/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx diff --git a/data/2016/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx b/2016/data/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx similarity index 100% rename from data/2016/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx rename to 2016/data/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx diff --git a/data/2016/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx b/2016/data/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx similarity index 100% rename from data/2016/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx rename to 2016/data/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx diff --git a/data/2016/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx b/2016/data/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx similarity index 100% rename from data/2016/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx rename to 2016/data/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx diff --git a/data/2016/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx b/2016/data/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx similarity index 100% rename from data/2016/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx rename to 2016/data/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx diff --git a/data/2016/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx b/2016/data/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx similarity index 100% rename from data/2016/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx rename to 2016/data/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx diff --git a/data/2016/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx b/2016/data/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx similarity index 100% rename from data/2016/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx rename to 2016/data/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx diff --git a/data/2016/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx b/2016/data/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx similarity index 100% rename from data/2016/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx rename to 2016/data/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx diff --git a/data/2016/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx b/2016/data/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx similarity index 100% rename from data/2016/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx rename to 2016/data/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx diff --git a/data/2016/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx b/2016/data/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx similarity index 100% rename from data/2016/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx rename to 2016/data/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx diff --git a/data/2016/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx b/2016/data/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx similarity index 100% rename from data/2016/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx rename to 2016/data/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx diff --git a/data/2016/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx b/2016/data/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx similarity index 100% rename from data/2016/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx rename to 2016/data/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx diff --git a/data/2016/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx b/2016/data/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx similarity index 100% rename from data/2016/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx rename to 2016/data/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx diff --git a/data/2016/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx b/2016/data/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx similarity index 100% rename from data/2016/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx rename to 2016/data/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx diff --git a/data/2016/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls b/2016/data/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls similarity index 100% rename from data/2016/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls rename to 2016/data/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls diff --git a/data/2016/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx b/2016/data/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx similarity index 100% rename from data/2016/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx rename to 2016/data/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx diff --git a/data/2016/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx b/2016/data/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx similarity index 100% rename from data/2016/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx rename to 2016/data/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx diff --git a/data/2016/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx b/2016/data/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx similarity index 100% rename from data/2016/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx rename to 2016/data/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx diff --git a/data/2016/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx b/2016/data/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx similarity index 100% rename from data/2016/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx rename to 2016/data/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx diff --git a/data/2016/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx b/2016/data/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx similarity index 100% rename from data/2016/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx rename to 2016/data/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx diff --git a/data/2016/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx b/2016/data/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx similarity index 100% rename from data/2016/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx rename to 2016/data/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx diff --git a/data/2016/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx b/2016/data/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx similarity index 100% rename from data/2016/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx rename to 2016/data/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx diff --git a/data/2016/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx b/2016/data/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx similarity index 100% rename from data/2016/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx rename to 2016/data/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx diff --git a/data/2016/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx b/2016/data/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx similarity index 100% rename from data/2016/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx rename to 2016/data/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx diff --git a/data/2016/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx b/2016/data/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx similarity index 100% rename from data/2016/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx rename to 2016/data/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx diff --git a/data/2016/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx b/2016/data/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx similarity index 100% rename from data/2016/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx rename to 2016/data/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx diff --git a/data/2016/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx b/2016/data/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx similarity index 100% rename from data/2016/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx rename to 2016/data/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx diff --git a/data/2016/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx b/2016/data/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx similarity index 100% rename from data/2016/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx rename to 2016/data/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx diff --git a/data/2016/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx b/2016/data/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx similarity index 100% rename from data/2016/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx rename to 2016/data/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx diff --git a/data/2016/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx b/2016/data/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx similarity index 100% rename from data/2016/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx rename to 2016/data/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx diff --git a/data/2016/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx b/2016/data/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx similarity index 100% rename from data/2016/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx rename to 2016/data/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx diff --git a/data/2016/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx b/2016/data/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx similarity index 100% rename from data/2016/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx rename to 2016/data/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx diff --git a/data/2016/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx b/2016/data/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx similarity index 100% rename from data/2016/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx rename to 2016/data/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx diff --git a/data/2016/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx b/2016/data/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx similarity index 100% rename from data/2016/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx rename to 2016/data/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx diff --git a/data/2016/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx b/2016/data/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx similarity index 100% rename from data/2016/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx rename to 2016/data/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx diff --git a/data/2016/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx b/2016/data/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx similarity index 100% rename from data/2016/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx rename to 2016/data/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx diff --git a/data/2016/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx b/2016/data/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx similarity index 100% rename from data/2016/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx rename to 2016/data/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx diff --git a/data/2016/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx b/2016/data/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx similarity index 100% rename from data/2016/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx rename to 2016/data/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx diff --git a/data/2016/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx b/2016/data/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx similarity index 100% rename from data/2016/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx rename to 2016/data/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx diff --git a/data/2016/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx b/2016/data/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx similarity index 100% rename from data/2016/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx rename to 2016/data/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx diff --git a/data/2016/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx b/2016/data/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx similarity index 100% rename from data/2016/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx rename to 2016/data/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx diff --git a/data/2016/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls b/2016/data/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls similarity index 100% rename from data/2016/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls rename to 2016/data/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls diff --git a/data/2016/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx b/2016/data/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx similarity index 100% rename from data/2016/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx rename to 2016/data/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx diff --git a/data/2016/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx b/2016/data/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx similarity index 100% rename from data/2016/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx rename to 2016/data/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx diff --git a/data/2016/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx b/2016/data/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx similarity index 100% rename from data/2016/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx rename to 2016/data/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx diff --git a/data/2016/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx b/2016/data/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx similarity index 100% rename from data/2016/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx rename to 2016/data/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx diff --git a/data/2016/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx b/2016/data/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx similarity index 100% rename from data/2016/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx rename to 2016/data/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx diff --git a/data/2016/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx b/2016/data/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx similarity index 100% rename from data/2016/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx rename to 2016/data/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx diff --git a/data/2016/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx b/2016/data/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx similarity index 100% rename from data/2016/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx rename to 2016/data/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx diff --git a/data/2016/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx b/2016/data/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx similarity index 100% rename from data/2016/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx rename to 2016/data/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx diff --git a/data/2016/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx b/2016/data/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx similarity index 100% rename from data/2016/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx rename to 2016/data/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx diff --git a/data/2016/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx b/2016/data/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx similarity index 100% rename from data/2016/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx rename to 2016/data/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx diff --git a/data/2016/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx b/2016/data/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx similarity index 100% rename from data/2016/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx rename to 2016/data/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx diff --git a/data/2016/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx b/2016/data/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx similarity index 100% rename from data/2016/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx rename to 2016/data/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx diff --git a/data/2016/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx b/2016/data/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx similarity index 100% rename from data/2016/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx rename to 2016/data/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx diff --git a/data/2016/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx b/2016/data/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx similarity index 100% rename from data/2016/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx rename to 2016/data/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx diff --git a/data/2016/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx b/2016/data/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx similarity index 100% rename from data/2016/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx rename to 2016/data/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx diff --git a/data/2016/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx b/2016/data/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx similarity index 100% rename from data/2016/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx rename to 2016/data/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx diff --git a/data/2016/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx b/2016/data/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx similarity index 100% rename from data/2016/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx rename to 2016/data/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx diff --git a/data/2016/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx b/2016/data/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx similarity index 100% rename from data/2016/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx rename to 2016/data/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx diff --git a/data/2016/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx b/2016/data/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx similarity index 100% rename from data/2016/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx rename to 2016/data/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx diff --git a/data/2016/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx b/2016/data/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx similarity index 100% rename from data/2016/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx rename to 2016/data/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx diff --git a/data/2016/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx b/2016/data/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx similarity index 100% rename from data/2016/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx rename to 2016/data/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx diff --git a/data/2016/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx b/2016/data/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx similarity index 100% rename from data/2016/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx rename to 2016/data/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx diff --git a/data/2016/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx b/2016/data/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx similarity index 100% rename from data/2016/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx rename to 2016/data/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx diff --git a/data/2016/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx b/2016/data/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx similarity index 100% rename from data/2016/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx rename to 2016/data/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx diff --git a/data/2016/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx b/2016/data/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx similarity index 100% rename from data/2016/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx rename to 2016/data/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx diff --git a/data/2016/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx b/2016/data/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx similarity index 100% rename from data/2016/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx rename to 2016/data/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx diff --git a/data/2016/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx b/2016/data/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx similarity index 100% rename from data/2016/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx rename to 2016/data/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx diff --git a/data/2016/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx b/2016/data/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx similarity index 100% rename from data/2016/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx rename to 2016/data/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx diff --git a/data/2016/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx b/2016/data/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx similarity index 100% rename from data/2016/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx rename to 2016/data/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx diff --git a/2016/docs/codebase-summary.md b/2016/docs/codebase-summary.md new file mode 100644 index 0000000..29626e7 --- /dev/null +++ b/2016/docs/codebase-summary.md @@ -0,0 +1,63 @@ +# Codebase Summary + +## Directory layout + +``` +thptqg2016/ +├── data/ # Source Excel files (~100, mixed formats) +├── scripts/ +│ └── build-database.js # Parse Excel → SQLite (build-time, Node + better-sqlite3) +├── public/ +│ └── thptqg2016.db # Generated DB, gzipped during CI +├── src/ +│ ├── main.jsx # React entry +│ ├── App.jsx # Root: tabs, lookup logic, useSqlite wiring +│ ├── App.css / index.css # Design tokens, dark mode, a11y styles +│ ├── hooks/ +│ │ └── use-sqlite.js # Fetch .db.gz + decompress + init sql.js +│ └── components/ +│ ├── search-form.jsx # Input for exam ID / full name +│ ├── score-table.jsx # Result table for lookups +│ └── custom-query.jsx # SQL editor + presets + result grid +├── .github/workflows/deploy.yml # CI: build db → gzip → vite build → Pages +├── vite.config.js # base: "/thptqg2016/" +└── eslint.config.js +``` + +## Key modules + +### `scripts/build-database.js` +Build-time only. Reads every `.xlsx/.xls` in `data/`, detects the header format (three variants), parses the `DIEM_THI` string via regex or separate score columns, normalizes gender, derives a diacritics-stripped `ho_ten_ascii` column for accent-insensitive search, and inserts into SQLite with three indexes (`ho_ten`, `ho_ten_ascii`, `ten_cum_thi`). + +### `src/hooks/use-sqlite.js` +Streams `.db.gz` with download progress, decompresses via `DecompressionStream("gzip")`, loads `sql.js` (WASM served from the `sql.js.org` CDN), and returns `{ db, loading, error, progress }`. + +### `src/App.jsx` +Two tabs: **Lookup** and **Custom SQL**. Lookup auto-detects exam IDs (regex `^[A-Z]{2,4}\d+$`) vs names and picks one of three query paths: exact exam ID / ASCII LIKE / original + ASCII LIKE. Capped at 100 rows. + +### `src/components/custom-query.jsx` +Whitelists leading keywords (`SELECT`, `PRAGMA`, `EXPLAIN`, `WITH`), auto-appends `LIMIT 1000` when missing, measures `performance.now()` execution time, and ships 7 preset analytics queries. + +## `student` table schema + +```sql +so_bao_danh TEXT PRIMARY KEY -- exam ID +ho_ten TEXT NOT NULL -- full name +ho_ten_ascii TEXT NOT NULL -- diacritics stripped, lowercased +ngay_sinh TEXT -- date of birth +ten_cum_thi TEXT -- exam cluster name +gioi_tinh TEXT -- "Nam" | "Nữ" | NULL +toan, ngu_van, vat_ly, hoa_hoc, -- REAL (nullable) subject scores +sinh_hoc, lich_su, dia_ly, +tieng_anh, tieng_phap, tieng_duc, +tieng_nhat, tieng_trung +``` + +Indexes: `idx_ho_ten`, `idx_ho_ten_ascii`, `idx_ten_cum_thi`. + +## Conventions + +- JS/JSX filenames: **kebab-case** (e.g., `search-form.jsx`, `use-sqlite.js`) +- React components: `PascalCase` named exports +- UI strings: Vietnamese (target audience) +- Code comments: English; explain *why*, not *what* diff --git a/2016/docs/deployment-guide.md b/2016/docs/deployment-guide.md new file mode 100644 index 0000000..87e4a03 --- /dev/null +++ b/2016/docs/deployment-guide.md @@ -0,0 +1,50 @@ +# Deployment Guide + +## Automatic (recommended) + +Push to `main` → GitHub Actions builds and deploys to GitHub Pages automatically. + +Workflow: `.github/workflows/deploy.yml` + +CI steps: +1. `npm ci` +2. `npm run build:db` — generate `public/thptqg2016.db` from `data/*.xlsx` +3. `gzip -k -9 public/thptqg2016.db` — max compression, keep original +4. `npm run build` — Vite bundles `dist/` +5. `rm -f dist/thptqg2016.db` — ship only the gzipped copy +6. `actions/upload-pages-artifact@v3` + `actions/deploy-pages@v4` + +One-time setup: **Settings → Pages → Source: GitHub Actions**. + +## Manual (local verification) + +```bash +npm install +npm run build:db +gzip -k -9 public/thptqg2016.db # Linux/macOS; on Windows use 7zip or WSL +npm run build +rm dist/thptqg2016.db # optional, shrinks artifact +npm run preview # serve dist/ locally +``` + +Open . + +## Base path + +`vite.config.js` sets `base: "/thptqg2016/"`. If you fork under a different repo name, update this to match `` so assets resolve correctly on GitHub Pages. + +## Updating data + +1. Add the new Excel file to `data/`. +2. If its header is unfamiliar, open `scripts/build-database.js` and extend `detectFormat()` or the `processSeparateScoresRow` / `processMappedRow` helpers. +3. Run `npm run build:db` locally to check row counts and error skips. +4. Commit + push → CI redeploys. + +## Troubleshooting + +| Symptom | Typical cause | +|---------|---------------| +| Blank page, 404 on assets | `base` in `vite.config.js` doesn't match the repo name | +| `Failed to fetch database: 404` | gzip step skipped, or `.db.gz` removed from `dist/` | +| WASM fails to load | `sql.js.org` blocked / offline — self-host `sql-wasm.wasm` in `public/` and update `SQL_WASM_URL` in `use-sqlite.js` | +| Missing rows after build | Excel file has an unknown header — check console for `Failed to read` or `errorCount` | diff --git a/2016/docs/project-overview-pdr.md b/2016/docs/project-overview-pdr.md new file mode 100644 index 0000000..9249e0a --- /dev/null +++ b/2016/docs/project-overview-pdr.md @@ -0,0 +1,34 @@ +# Project Overview — thptqg2016 + +## Goal + +Provide a public lookup tool for Vietnam's 2016 National High School Graduation Exam scores (877,461 candidates), running entirely on the client, hosted for free on GitHub Pages. + +## Scope + +- Lookup by exam ID or full name (with Vietnamese diacritics handling) +- Read-only SQL queries against a single `student` table +- Static dataset — no updates (the 2016 exam is long over) + +## Target users + +- Former 2016 candidates checking their scores +- Education researchers / data journalists running aggregate stats +- Developers exploring SQL on a real-world dataset + +## Constraints + +- **Zero backend**: the full DB (tens of MB gzipped) is downloaded to the browser +- **Read-only**: INSERT/UPDATE/DELETE rejected to avoid the illusion that user edits persist +- **Row caps**: 100 rows (lookup), 1000 rows (SQL) to prevent browser hangs +- **Vietnamese-first UI**: app labels and data are Vietnamese; documentation is English + +## Data sources + +Excel files (`.xlsx`/`.xls`) collected from newspapers and exam clusters in 2016, stored in `data/`. One file per cluster, with several different column layouts (see `scripts/build-database.js`). + +Original aggregator link: — no longer accessible, so the raw Excel files are mirrored in this repo under `data/`. + +## Status + +Stable. Data is frozen. Recent work focuses on UX polish (dark mode, accessibility, diacritics-insensitive search). diff --git a/2016/docs/system-architecture.md b/2016/docs/system-architecture.md new file mode 100644 index 0000000..4c98b2d --- /dev/null +++ b/2016/docs/system-architecture.md @@ -0,0 +1,83 @@ +# System Architecture + +## Overview + +A **static serverless** design: the entire dataset is packaged into a single SQLite file, gzip-compressed, and served as a static asset via GitHub Pages. The browser downloads it, decompresses it, and queries it in-process using `sql.js` (SQLite compiled to WebAssembly). + +``` +┌─────────────────┐ build ┌──────────────────────┐ +│ data/*.xlsx │ ───────────▶ │ scripts/ │ +│ (mixed formats)│ │ build-database.js │ +└─────────────────┘ │ (Node + xlsx + │ + │ better-sqlite3) │ + └──────────┬───────────┘ + │ + ▼ + ┌──────────────────────┐ + │ public/thptqg2016.db │ + └──────────┬───────────┘ + │ gzip -9 (CI) + ▼ + ┌──────────────────────┐ + │ dist/thptqg2016.db.gz│ + │ dist/assets/* │ ◀── Vite build + └──────────┬───────────┘ + │ upload-pages-artifact + ▼ + GitHub Pages CDN + │ + ▼ + ┌──────────────────────────────────┐ + │ Browser │ + │ ┌────────────────────────────┐ │ + │ │ useSqlite hook │ │ + │ │ fetch(.db.gz) + stream │ │ + │ │ DecompressionStream gzip │ │ + │ │ sql.js WASM (from CDN) │ │ + │ └─────────────┬──────────────┘ │ + │ ▼ │ + │ ┌────────────────────────────┐ │ + │ │ React UI │ │ + │ │ - SearchForm / ScoreTable │ │ + │ │ - CustomQuery (SQL editor)│ │ + │ └────────────────────────────┘ │ + └──────────────────────────────────┘ +``` + +## Build-time data flow + +1. Developer drops Excel files into `data/`. +2. `npm run build:db` reads every file; for each one: + - Detects the header row against a `KNOWN_HEADERS` set. + - Picks a format: `separate-scores` (one column per subject) vs. `mapped` (single `DIEM_THI` string). + - Parses each row into a canonical 18-column record. + - `INSERT OR REPLACE` into SQLite (primary key = `so_bao_danh` handles duplicates). +3. `VACUUM` shrinks the file. +4. CI runs `gzip -k -9` → `.db.gz`. + +## Runtime flow + +1. Page loads → React mounts → `useSqlite("thptqg2016.db.gz")`. +2. Streaming fetch with a progress bar (driven by `Content-Length`). +3. `DecompressionStream("gzip")` decompresses on the fly. +4. `sql.js` loads its WASM from `https://sql.js.org/dist/sql-wasm.wasm`. +5. `new SQL.Database(Uint8Array)` — DB is now in RAM. +6. Each search / query → `db.prepare()` + `stmt.step()` loop → render. + +## Design decisions + +| Concern | Choice | Rationale | +|---------|--------|-----------| +| Storage | Static SQLite file | No backend needed; dataset is frozen | +| Compression | gzip in CI, `DecompressionStream` in browser | Native browser API; no extra library | +| WASM hosting | `sql.js.org` CDN | Smaller self-hosted artifact | +| Diacritics search | Pre-computed `ho_ten_ascii` column | `LOWER(REPLACE(...))` at query time defeats the index | +| SQL safety | Leading-keyword allowlist | `sql.js` is in-memory so writes don't persist, but the allowlist prevents user confusion | +| Row caps | 100 (lookup), 1000 (SQL) | Keep DOM render sizes reasonable | + +## Risks and limitations + +- **DB size**: tens of MB gzipped — slow links have a visible wait; mitigated by the progress bar. +- **Browser memory**: the full DB lives in RAM; older mobile devices may OOM. +- **Dependency on `sql.js.org`**: if that CDN is unreachable, WASM fails to load. +- **Excel format drift**: a new source file with an unseen header layout needs a new branch in `detectFormat()`. diff --git a/2016/eslint.config.js b/2016/eslint.config.js new file mode 100644 index 0000000..1f236de --- /dev/null +++ b/2016/eslint.config.js @@ -0,0 +1,31 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; + +export default [ + { ignores: ["dist"] }, + { + files: ["**/*.{js,jsx}"], + languageOptions: { + ecmaVersion: 2024, + globals: globals.browser, + parserOptions: { + ecmaVersion: "latest", + ecmaFeatures: { jsx: true }, + sourceType: "module", + }, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, +]; diff --git a/2016/index.html b/2016/index.html new file mode 100644 index 0000000..172159e --- /dev/null +++ b/2016/index.html @@ -0,0 +1,13 @@ + + + + + + + Tra cứu điểm thi THPT QG 2016 + + +
+ + + diff --git a/package.json b/2016/package.json similarity index 55% rename from package.json rename to 2016/package.json index d5cc3c6..245c829 100644 --- a/package.json +++ b/2016/package.json @@ -1,22 +1,21 @@ { - "name": "thptqg", + "name": "thptqg2016", "private": true, + "packageManager": "pnpm@11.1.1", "version": "1.0.0", "type": "module", - "description": "Tra cứu điểm thi THPT Quốc gia — 2016 và 2017", + "description": "Tra cứu điểm thi THPT QG 2016 - Hỗ trợ truy vấn SQL tùy chỉnh", "scripts": { - "build:rust": "cargo build --release --manifest-path parser/Cargo.toml", - "build:db": "node parser/scripts/build-db.js", + "build:rust": "cargo build --release --manifest-path tools/xlsxread/Cargo.toml", + "build:db": "cargo build --release --manifest-path tools/xlsxread/Cargo.toml && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2016-data.toml --input data --output public/thptqg2016.db", "dev": "vite", "build": "vite build", - "assemble": "node scripts/assemble-site.js", - "build:site": "npm run build && npm run assemble", "preview": "vite preview", "lint": "eslint ." }, "repository": { "type": "git", - "url": "git+https://github.com/tiennm99/thptqg.git" + "url": "git+https://github.com/tiennm99/thptqg2016.git" }, "license": "ISC", "dependencies": { diff --git a/2016/pnpm-lock.yaml b/2016/pnpm-lock.yaml new file mode 100644 index 0000000..ce02b86 --- /dev/null +++ b/2016/pnpm-lock.yaml @@ -0,0 +1,1624 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^19.2.4 + version: 19.2.6 + react-dom: + specifier: ^19.2.4 + version: 19.2.6(react@19.2.6) + sql.js: + specifier: ^1.14.1 + version: 1.14.1 + devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.16) + eslint: + specifier: ^9.39.4 + version: 9.39.4 + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.1.1(eslint@9.39.4) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.4) + globals: + specifier: ^17.4.0 + version: 17.6.0 + vite: + specifier: ^8.0.16 + version: 8.0.16 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.10.29: + resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.353: + resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sql.js@1.14.1: + resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.133.0': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@rolldown/pluginutils@1.0.1': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.0.1(vite@8.0.16)': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.16 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.10.29: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.29 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.353 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001792: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.353: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + eslint: 9.39.4 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@17.6.0: {} + + has-flag@4.0.0: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.38: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + resolve-from@4.0.0: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + sql.js@1.14.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.16: + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/2016/pnpm-workspace.yaml b/2016/pnpm-workspace.yaml new file mode 100644 index 0000000..65783dc --- /dev/null +++ b/2016/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + better-sqlite3: true diff --git a/2016/src/App.css b/2016/src/App.css new file mode 100644 index 0000000..9410c19 --- /dev/null +++ b/2016/src/App.css @@ -0,0 +1,474 @@ +.app { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem 3rem; +} + +/* Header */ +header { + text-align: center; + margin-bottom: 2rem; +} + +header h1 { + font-size: clamp(1.4rem, 2.4vw, 2rem); + color: var(--text); + margin: 0 0 0.4rem; + letter-spacing: -0.01em; + font-weight: 700; +} + +.subtitle { + color: var(--text-muted); + font-size: 0.95rem; + margin: 0; +} + +.subtitle .pill { + display: inline-block; + padding: 0.1rem 0.55rem; + background: var(--accent-soft); + color: var(--accent); + border-radius: 999px; + font-weight: 600; + font-size: 0.8rem; + margin-right: 0.35rem; +} + +/* Tabs */ +.tabs { + display: flex; + gap: 0; + max-width: 600px; + margin: 0 auto 1.5rem; + border-bottom: 1px solid var(--border); +} + +.tab { + flex: 1; + padding: 0.75rem 1rem; + font-size: 0.95rem; + background: none; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + cursor: pointer; + color: var(--text-muted); + font-weight: 500; + transition: color 0.15s, border-color 0.15s; +} + +.tab:hover { + color: var(--accent); +} + +.tab[aria-selected="true"] { + color: var(--accent); + border-bottom-color: var(--accent); + font-weight: 600; +} + +/* Search form */ +.search-form { + display: flex; + gap: 0.5rem; + max-width: 600px; + margin: 0 auto 1.5rem; + position: relative; +} + +.search-field { + flex: 1; + position: relative; +} + +.search-form input { + width: 100%; + padding: 0.75rem 2.25rem 0.75rem 1rem; + font-size: 1rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--surface); + color: var(--text); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.search-form input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} + +.search-clear { + position: absolute; + right: 0.4rem; + top: 50%; + transform: translateY(-50%); + width: 1.75rem; + height: 1.75rem; + display: grid; + place-items: center; + border: none; + background: transparent; + color: var(--text-faint); + font-size: 1.1rem; + border-radius: 50%; + cursor: pointer; +} + +.search-clear:hover { + background: var(--surface-alt); + color: var(--text); +} + +.search-form button[type="submit"] { + padding: 0.75rem 1.5rem; + font-size: 0.95rem; + font-weight: 600; + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--radius); + cursor: pointer; + transition: background 0.15s, transform 0.05s; +} + +.search-form button[type="submit"]:hover:not(:disabled) { + background: var(--accent-hover); +} + +.search-form button[type="submit"]:active:not(:disabled) { + transform: translateY(1px); +} + +.search-form button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Custom query */ +.custom-query { + max-width: 960px; + margin: 0 auto; +} + +.preset-list { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-bottom: 0.85rem; + align-items: center; +} + +.preset-label { + font-size: 0.8rem; + color: var(--text-muted); + white-space: nowrap; + margin-right: 0.25rem; + font-weight: 500; +} + +.preset-btn { + padding: 0.35rem 0.75rem; + font-size: 0.8rem; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + border-radius: 999px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; +} + +.preset-btn:hover:not(:disabled) { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--accent); +} + +.preset-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.query-form { + margin-bottom: 1rem; +} + +.query-form textarea { + width: 100%; + padding: 0.85rem 1rem; + font-size: 0.9rem; + font-family: + "JetBrains Mono", "Cascadia Code", "Fira Code", ui-monospace, monospace; + background: var(--surface); + color: var(--text); + border: 1px solid var(--border-strong); + border-radius: var(--radius); + outline: none; + resize: vertical; + min-height: 130px; + transition: border-color 0.15s, box-shadow 0.15s; + tab-size: 2; + line-height: 1.5; +} + +.query-form textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} + +.query-actions { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 0.6rem; + flex-wrap: wrap; +} + +.query-actions button { + padding: 0.6rem 1.4rem; + font-size: 0.9rem; + font-weight: 600; + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--radius); + cursor: pointer; + transition: background 0.15s; +} + +.query-actions button:hover:not(:disabled) { + background: var(--accent-hover); +} + +.query-actions button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.exec-time { + font-size: 0.85rem; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + +.exec-time.running { + color: var(--accent); +} + +.kbd { + display: inline-block; + padding: 0.05rem 0.4rem; + font-family: ui-monospace, monospace; + font-size: 0.75rem; + background: var(--surface-alt); + border: 1px solid var(--border); + border-bottom-width: 2px; + border-radius: 4px; + color: var(--text-muted); +} + +/* Loading */ +.loading { + max-width: 600px; + margin: 0.5rem auto 1.5rem; + padding: 0.85rem 1rem; + text-align: center; + color: var(--text-muted); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); +} + +.loading p { + margin: 0 0 0.5rem; + font-size: 0.9rem; +} + +.progress-bar { + width: 100%; + max-width: 300px; + height: 6px; + background: var(--surface-alt); + border-radius: 4px; + margin: 0 auto; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--accent-hover)); + border-radius: 4px; + transition: width 0.3s; +} + +/* Results table */ +.table-wrapper { + overflow-x: auto; + margin-top: 1rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); +} + +.result-count { + color: var(--text-muted); + font-size: 0.9rem; + margin: 0.5rem 0; + padding: 0 0.25rem; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +thead th { + background: var(--surface-alt); + padding: 0.65rem 0.6rem; + text-align: left; + white-space: nowrap; + position: sticky; + top: 0; + border-bottom: 1px solid var(--border-strong); + color: var(--text-muted); + font-weight: 600; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +tbody td { + padding: 0.55rem 0.6rem; + border-bottom: 1px solid var(--border); +} + +tbody tr:last-child td { + border-bottom: none; +} + +tbody tr:nth-child(even) td { + background: color-mix(in srgb, var(--surface-alt) 45%, transparent); +} + +tbody tr:hover td { + background: var(--accent-soft); +} + +.sbd-cell { + font-family: ui-monospace, monospace; + font-size: 0.85rem; + color: var(--text-muted); + white-space: nowrap; +} + +.name-cell { + white-space: nowrap; + font-weight: 500; +} + +.cumthi-cell { + font-size: 0.82rem; + color: var(--text-muted); + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.score-cell { + text-align: center; + font-variant-numeric: tabular-nums; +} + +.score-cell.score-high { + color: var(--ok); + font-weight: 600; +} + +.score-cell.score-low { + color: var(--danger); +} + +.score-cell.score-empty { + color: var(--text-faint); +} + +/* Messages */ +.no-results, +.error, +.warning { + text-align: center; + padding: 0.9rem 1rem; + border-radius: var(--radius); + margin: 1rem auto; + max-width: 600px; + font-size: 0.9rem; +} + +.no-results { + color: var(--text-muted); + background: var(--surface); + border: 1px dashed var(--border-strong); +} + +.error { + color: var(--danger); + background: var(--danger-bg); + border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent); +} + +.warning { + color: var(--warn); + background: var(--warn-bg); +} + +/* Footer */ +footer { + text-align: center; + margin-top: 3rem; + padding-top: 1rem; + border-top: 1px solid var(--border); + color: var(--text-faint); + font-size: 0.85rem; +} + +footer a { + color: var(--text-muted); +} + +/* Responsive */ +@media (max-width: 640px) { + .app { + padding: 1.25rem 0.75rem 2rem; + } + + .search-form { + flex-direction: column; + } + + .search-form button[type="submit"] { + width: 100%; + } + + table { + font-size: 0.8rem; + } + + thead th, + tbody td { + padding: 0.45rem 0.4rem; + } + + .preset-list { + flex-direction: column; + align-items: stretch; + } + + .preset-btn { + text-align: left; + } +} diff --git a/2016/src/App.jsx b/2016/src/App.jsx new file mode 100644 index 0000000..494519c --- /dev/null +++ b/2016/src/App.jsx @@ -0,0 +1,174 @@ +import { useState, useCallback, useEffect } from "react"; +import { useSqlite } from "./hooks/use-sqlite"; +import { SearchForm } from "./components/search-form"; +import { ScoreTable } from "./components/score-table"; +import { CustomQuery } from "./components/custom-query"; +import "./App.css"; + +const DB_URL = import.meta.env.BASE_URL + "thptqg2016.db.gz"; +const MAX_RESULTS = 100; + +// Strip Vietnamese diacritics for search: "nguyễn bữu lộc" → "nguyen buu loc" +function toAscii(str) { + return str + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/đ/gi, "d") + .toLowerCase(); +} + +// Check if string contains only ASCII (no Vietnamese diacritics) +function isAsciiOnly(str) { + return /^[\x00-\x7F]*$/.test(str); +} + +function App() { + const { db, loading, error, progress } = useSqlite(DB_URL); + const [results, setResults] = useState(null); + const [searchError, setSearchError] = useState(null); + const [activeTab, setActiveTab] = useState("search"); + + const handleSearch = useCallback( + (query) => { + if (!db) return; + setSearchError(null); + + try { + const isExamId = /^[A-Z]{2,4}\d+$/i.test(query) || /^\d+$/.test(query); + let stmt; + + if (isExamId) { + stmt = db.prepare( + "SELECT * FROM student WHERE so_bao_danh = $q LIMIT $limit", + ); + stmt.bind({ $q: query.toUpperCase(), $limit: MAX_RESULTS }); + } else if (isAsciiOnly(query)) { + // ASCII input: search against normalized column (diacritics-insensitive) + stmt = db.prepare( + "SELECT * FROM student WHERE ho_ten_ascii LIKE $q LIMIT $limit", + ); + stmt.bind({ $q: `%${toAscii(query)}%`, $limit: MAX_RESULTS }); + } else { + // Vietnamese input: search both original and normalized + const normalized = toAscii(query); + stmt = db.prepare( + `SELECT * FROM student + WHERE ho_ten LIKE $q OR ho_ten_ascii LIKE $qn + LIMIT $limit`, + ); + stmt.bind({ + $q: `%${query}%`, + $qn: `%${normalized}%`, + $limit: MAX_RESULTS, + }); + } + + const rows = []; + while (stmt.step()) { + rows.push(stmt.getAsObject()); + } + stmt.free(); + setResults(rows); + } catch (err) { + setSearchError(err.message); + } + }, + [db], + ); + + // Ctrl+Enter shortcut to execute query in SQL tab + useEffect(() => { + function handleKeyDown(e) { + if (e.ctrlKey && e.key === "Enter" && activeTab === "sql") { + const form = document.querySelector(".query-form"); + if (form) form.requestSubmit(); + } + } + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [activeTab]); + + return ( +
+
+

Tra cứu điểm thi THPT Quốc gia 2016

+

+ 877.461 thí sinh + Dữ liệu toàn quốc · Hỗ trợ truy vấn SQL tùy chỉnh +

+
+ +
+ {loading && ( +
+

+ Đang tải cơ sở dữ liệu{progress > 0 ? ` · ${progress}%` : "..."} +

+
+
+
+
+ )} + + {error &&

Lỗi: {error}

} + +
+ + +
+ + {activeTab === "search" && ( + <> + + + {searchError && ( +

Lỗi truy vấn: {searchError}

+ )} + + + + {results && results.length >= MAX_RESULTS && ( +

+ Hiển thị tối đa {MAX_RESULTS} kết quả. Vui lòng tìm kiếm cụ + thể hơn. +

+ )} + + )} + + {activeTab === "sql" && ( + + )} +
+ +
+

+ Nguồn: Sưu tầm từ trang báo thời đó · Dữ liệu chỉ mang tính tham + khảo +

+
+
+ ); +} + +export default App; diff --git a/src/components/custom-query.jsx b/2016/src/components/custom-query.jsx similarity index 59% rename from src/components/custom-query.jsx rename to 2016/src/components/custom-query.jsx index 21b7af5..5b4ed01 100644 --- a/src/components/custom-query.jsx +++ b/2016/src/components/custom-query.jsx @@ -1,12 +1,79 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback } from "react"; const MAX_ROWS = 1000; -export function CustomQuery({ db, disabled, presets = [] }) { - // Every preset set ends with a "Hệ thống" group whose first query dumps the - // table schema; that is what gets auto-run when the tab first opens. - const schemaPreset = presets[presets.length - 1]?.queries[0]; +const PRESET_QUERIES = [ + { + label: "Top 10 điểm Toán cao nhất", + sql: `SELECT so_bao_danh, ho_ten, ten_cum_thi, toan +FROM student WHERE toan IS NOT NULL +ORDER BY toan DESC LIMIT 10`, + }, + { + label: "Điểm trung bình theo cụm thi", + sql: `SELECT ten_cum_thi, + COUNT(*) AS so_luong, + ROUND(AVG(toan), 2) AS tb_toan, + ROUND(AVG(ngu_van), 2) AS tb_van, + ROUND(AVG(tieng_anh), 2) AS tb_anh +FROM student +GROUP BY ten_cum_thi +ORDER BY so_luong DESC +LIMIT 20`, + }, + { + label: "Thống kê theo giới tính", + sql: `SELECT gioi_tinh, + COUNT(*) AS so_luong, + ROUND(AVG(toan), 2) AS tb_toan, + ROUND(AVG(ngu_van), 2) AS tb_van +FROM student +WHERE gioi_tinh IS NOT NULL +GROUP BY gioi_tinh`, + }, + { + label: "Thí sinh đạt 9+ điểm Toán", + sql: `SELECT so_bao_danh, ho_ten, ten_cum_thi, gioi_tinh, toan +FROM student WHERE toan >= 9 +ORDER BY toan DESC LIMIT 50`, + }, + { + label: "Phân bố điểm Toán", + sql: `SELECT + CASE + WHEN toan < 1 THEN '0-1' + WHEN toan < 2 THEN '1-2' + WHEN toan < 3 THEN '2-3' + WHEN toan < 4 THEN '3-4' + WHEN toan < 5 THEN '4-5' + WHEN toan < 6 THEN '5-6' + WHEN toan < 7 THEN '6-7' + WHEN toan < 8 THEN '7-8' + WHEN toan < 9 THEN '8-9' + ELSE '9-10' + END AS khoang_diem, + COUNT(*) AS so_luong +FROM student WHERE toan IS NOT NULL +GROUP BY khoang_diem +ORDER BY khoang_diem`, + }, + { + label: "Số thí sinh theo ngoại ngữ", + sql: `SELECT + SUM(CASE WHEN tieng_anh IS NOT NULL THEN 1 ELSE 0 END) AS tieng_anh, + SUM(CASE WHEN tieng_phap IS NOT NULL THEN 1 ELSE 0 END) AS tieng_phap, + SUM(CASE WHEN tieng_duc IS NOT NULL THEN 1 ELSE 0 END) AS tieng_duc, + SUM(CASE WHEN tieng_nhat IS NOT NULL THEN 1 ELSE 0 END) AS tieng_nhat, + SUM(CASE WHEN tieng_trung IS NOT NULL THEN 1 ELSE 0 END) AS tieng_trung +FROM student`, + }, + { + label: "Schema bảng student", + sql: `PRAGMA table_info(student)`, + }, +]; +export function CustomQuery({ db, disabled }) { const [sql, setSql] = useState(""); const [columns, setColumns] = useState([]); const [rows, setRows] = useState([]); @@ -78,36 +145,19 @@ export function CustomQuery({ db, disabled, presets = [] }) { executeQuery(presetSql); } - // Auto-run the schema preset the first time the tab is opened so the user - // sees the student columns instead of a blank textarea. - useEffect(() => { - if (db && schemaPreset && columns.length === 0 && !sql) { - // eslint-disable-next-line react-hooks/set-state-in-effect - executeQuery(schemaPreset.sql); - setSql(schemaPreset.sql); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [db]); - return (
-
- {presets.map((group) => ( -
- {group.category} -
- {group.queries.map((p, i) => ( - - ))} -
-
+
+ Mẫu truy vấn: + {PRESET_QUERIES.map((p, i) => ( + ))}
@@ -122,7 +172,8 @@ export function CustomQuery({ db, disabled, presets = [] }) { />
{execTime !== null && ( diff --git a/2016/src/components/score-table.jsx b/2016/src/components/score-table.jsx new file mode 100644 index 0000000..e5258d6 --- /dev/null +++ b/2016/src/components/score-table.jsx @@ -0,0 +1,74 @@ +const SUBJECT_COLUMNS = [ + { key: "toan", label: "Toán" }, + { key: "ngu_van", label: "Ngữ văn" }, + { key: "vat_ly", label: "Vật lí" }, + { key: "hoa_hoc", label: "Hóa học" }, + { key: "sinh_hoc", label: "Sinh học" }, + { key: "lich_su", label: "Lịch sử" }, + { key: "dia_ly", label: "Địa lí" }, + { key: "tieng_anh", label: "T.Anh" }, + { key: "tieng_phap", label: "T.Pháp" }, + { key: "tieng_duc", label: "T.Đức" }, + { key: "tieng_nhat", label: "T.Nhật" }, + { key: "tieng_trung", label: "T.Trung" }, +]; + +function formatScore(val) { + if (val === null || val === undefined) return "—"; + return Number(val).toFixed(2); +} + +function scoreClass(val) { + if (val === null || val === undefined) return "score-cell score-empty"; + const num = Number(val); + if (num >= 8) return "score-cell score-high"; + if (num < 4) return "score-cell score-low"; + return "score-cell"; +} + +export function ScoreTable({ results }) { + if (!results) return null; + if (results.length === 0) { + return

Không tìm thấy kết quả.

; + } + + return ( + <> +

Tìm thấy {results.length} kết quả

+
+ + + + + + + + + {SUBJECT_COLUMNS.map((col) => ( + + ))} + + + + {results.map((row) => ( + + + + + + + {SUBJECT_COLUMNS.map((col) => ( + + ))} + + ))} + +
SBDHọ tênNgày sinhCụm thiGT{col.label}
{row.so_bao_danh}{row.ho_ten}{row.ngay_sinh || "—"} + {row.ten_cum_thi || "—"} + {row.gioi_tinh || "—"} + {formatScore(row[col.key])} +
+
+ + ); +} diff --git a/2016/src/components/search-form.jsx b/2016/src/components/search-form.jsx new file mode 100644 index 0000000..bec23f7 --- /dev/null +++ b/2016/src/components/search-form.jsx @@ -0,0 +1,46 @@ +import { useState } from "react"; + +export function SearchForm({ onSearch, disabled }) { + const [query, setQuery] = useState(""); + + function handleSubmit(e) { + e.preventDefault(); + const trimmed = query.trim(); + if (trimmed) onSearch(trimmed); + } + + return ( +
+
+ + setQuery(e.target.value)} + placeholder="Nhập SBD hoặc họ tên (VD: nguyen van a)..." + disabled={disabled} + autoFocus + autoComplete="off" + spellCheck={false} + /> + {query && ( + + )} +
+ +
+ ); +} diff --git a/2016/src/hooks/use-sqlite.js b/2016/src/hooks/use-sqlite.js new file mode 100644 index 0000000..71c6e3a --- /dev/null +++ b/2016/src/hooks/use-sqlite.js @@ -0,0 +1,83 @@ +import { useState, useEffect, useRef } from "react"; +import initSqlJs from "sql.js"; + +const SQL_WASM_URL = "https://sql.js.org/dist/sql-wasm.wasm"; + +/** + * Hook to load a SQLite database from a gzipped URL into sql.js. + * Returns { db, loading, error, progress }. + */ +export function useSqlite(dbUrl) { + const [db, setDb] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [progress, setProgress] = useState(0); + const dbRef = useRef(null); + + useEffect(() => { + let cancelled = false; + + async function load() { + try { + const SQL = await initSqlJs({ locateFile: () => SQL_WASM_URL }); + + const response = await fetch(dbUrl); + if (!response.ok) + throw new Error(`Failed to fetch database: ${response.status}`); + + const contentLength = +response.headers.get("Content-Length") || 0; + const reader = response.body.getReader(); + const chunks = []; + let received = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + received += value.length; + if (contentLength > 0) { + setProgress(Math.round((received / contentLength) * 100)); + } + } + + if (cancelled) return; + + const blob = new Blob(chunks); + let arrayBuffer; + + if (dbUrl.endsWith(".gz")) { + const ds = new DecompressionStream("gzip"); + const decompressed = blob.stream().pipeThrough(ds); + const decompressedBlob = await new Response(decompressed).blob(); + arrayBuffer = await decompressedBlob.arrayBuffer(); + } else { + arrayBuffer = await blob.arrayBuffer(); + } + + if (cancelled) return; + + const database = new SQL.Database(new Uint8Array(arrayBuffer)); + dbRef.current = database; + setDb(database); + setLoading(false); + } catch (err) { + if (!cancelled) { + setError(err.message); + setLoading(false); + } + } + } + + load(); + + return () => { + cancelled = true; + if (dbRef.current) { + dbRef.current.close(); + dbRef.current = null; + } + }; + }, [dbUrl]); + + return { db, loading, error, progress }; +} diff --git a/2016/src/index.css b/2016/src/index.css new file mode 100644 index 0000000..52bc39d --- /dev/null +++ b/2016/src/index.css @@ -0,0 +1,74 @@ +:root { + /* Light theme tokens */ + --bg: #f7f8fb; + --surface: #ffffff; + --surface-alt: #f2f4f9; + --border: #e3e6ee; + --border-strong: #cfd4e1; + --text: #1a1a2e; + --text-muted: #5c6275; + --text-faint: #8a8fa3; + --accent: #4361ee; + --accent-hover: #3a56d4; + --accent-soft: #e9edff; + --danger: #d32f2f; + --danger-bg: #fdecea; + --warn: #b45309; + --warn-bg: #fff4d6; + --ok: #0f8a4f; + --ok-bg: #e4f7ec; + --shadow-sm: 0 1px 2px rgba(20, 22, 38, 0.06); + --shadow-md: 0 4px 14px rgba(20, 22, 38, 0.08); + --radius: 10px; + --radius-sm: 6px; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0f1117; + --surface: #181b24; + --surface-alt: #21252f; + --border: #2a2f3d; + --border-strong: #3a4052; + --text: #eef0f6; + --text-muted: #a7adbe; + --text-faint: #6e7486; + --accent: #8aa1ff; + --accent-hover: #a6b8ff; + --accent-soft: #1e2647; + --danger: #ff7a7a; + --danger-bg: #3a1d1d; + --warn: #ffc875; + --warn-bg: #3a2a12; + --ok: #6ee2a4; + --ok-bg: #133524; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5); + --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.4); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + color-scheme: light dark; +} + +body { + margin: 0; + font-family: + "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 4px; +} diff --git a/2016/src/main.jsx b/2016/src/main.jsx new file mode 100644 index 0000000..fdce256 --- /dev/null +++ b/2016/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.jsx"; + +createRoot(document.getElementById("root")).render( + + + , +); diff --git a/2016/tools/sync-from-thptqg2017.sh b/2016/tools/sync-from-thptqg2017.sh new file mode 100755 index 0000000..fd86408 --- /dev/null +++ b/2016/tools/sync-from-thptqg2017.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Re-sync xlsxread Rust source from thptqg2017. +# +# Source SHA: 8b4a755c115595bf1b937d749eb1133efb3a6e22 (chore/xlsxread-rust) +# +# Re-run this when xlsxread is updated upstream. The configs/ directory is +# NOT synced — it contains thptqg2016-specific configs and test stubs that +# must be maintained here independently. +# +# Usage: +# ./tools/sync-from-thptqg2017.sh /path/to/thptqg2017 +# +set -euo pipefail + +SRC="${1:?Usage: $0 /path/to/thptqg2017}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEST="$SCRIPT_DIR/xlsxread" + +if [ ! -d "$SRC/tools/xlsxread" ]; then + echo "Error: $SRC/tools/xlsxread not found" >&2 + exit 1 +fi + +echo "Syncing from: $SRC/tools/xlsxread" +echo "Syncing to: $DEST" +echo "" + +# Sync source, tests, and Cargo manifests — exclude build artifacts and dataset configs +for item in src tests Cargo.toml Cargo.lock; do + if [ -e "$SRC/tools/xlsxread/$item" ]; then + cp -r "$SRC/tools/xlsxread/$item" "$DEST/" + echo " synced: $item" + fi +done + +echo "" +echo "Sync complete." +echo "Next steps:" +echo " 1. Review src/ for breaking changes to config.rs / writer.rs that" +echo " may affect format_detect_2016.rs or the thptqg2016-data.toml config." +echo " 2. Rebuild: cargo build --release --manifest-path $DEST/Cargo.toml" +echo " 3. Test: cargo test --manifest-path $DEST/Cargo.toml" +echo " 4. Commit on a chore/xlsxread-sync-... branch." diff --git a/parser/Cargo.lock b/2016/tools/xlsxread/Cargo.lock similarity index 100% rename from parser/Cargo.lock rename to 2016/tools/xlsxread/Cargo.lock diff --git a/parser/Cargo.toml b/2016/tools/xlsxread/Cargo.toml similarity index 100% rename from parser/Cargo.toml rename to 2016/tools/xlsxread/Cargo.toml diff --git a/2016/tools/xlsxread/configs/thptqg2016-data.toml b/2016/tools/xlsxread/configs/thptqg2016-data.toml new file mode 100644 index 0000000..646ad68 --- /dev/null +++ b/2016/tools/xlsxread/configs/thptqg2016-data.toml @@ -0,0 +1,86 @@ +# Config for thptqg2016 data/ — 4 .xls + 115 .xlsx mixed files. +# +# Three column layouts exist across the 119 files; the binary selects the +# right one per-file at runtime via format_detection = "thptqg2016": +# +# separate-scores header SBD(0)/HOTEN(1)/TOAN(2)... — dhhanghai files +# mapped header SOBAODANH|SBD + DIEM_THI — most provinces +# default no header; positional 6-col layout — remaining files +# +# Schema differences from thptqg2017: +# + ten_cum_thi TEXT (exam-cluster name, TEN_CUMTHI column) +# + gioi_tinh TEXT (gender: "Nam"/"Nữ", GIOI_TINH column) +# + tieng_duc REAL (German) +# + tieng_nhat REAL (Japanese) +# - khtn, khxh, tieng_nga (not in 2016 dataset) +# +# sheet_mode = "all": several provinces overflow into Sheet2 (65k Excel row cap). +# strip_blank_rows = false: no blank-row anomaly observed in this dataset. + +format_detection = "thptqg2016" + +[reader] +sheet_mode = "all" +strip_blank_rows = false + +[validation] +require_numeric_sbd = false +require_nonempty_name = true +require_nonempty_sbd = true + +[header] +# Tokens that identify a header row by first-cell content (uppercased). +# Covers both SOBAODANH-style and SBD-style headers. +tokens = ["SOBAODANH", "SBD", "HO_TEN", "HOTEN", "HỌ TÊN", "STT"] + +[schema] +ddl = """ +CREATE TABLE student ( + so_bao_danh TEXT PRIMARY KEY, + ho_ten TEXT NOT NULL, + ho_ten_ascii TEXT NOT NULL, + ngay_sinh TEXT, + ten_cum_thi TEXT, + gioi_tinh TEXT, + toan REAL, + ngu_van REAL, + vat_ly REAL, + hoa_hoc REAL, + sinh_hoc REAL, + lich_su REAL, + dia_ly REAL, + tieng_anh REAL, + tieng_phap REAL, + tieng_duc REAL, + tieng_nhat REAL, + tieng_trung REAL +); +CREATE INDEX idx_ho_ten ON student(ho_ten); +CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); +CREATE INDEX idx_ten_cum_thi ON student(ten_cum_thi); +""" + +[scores] +toan = 'Toán:\s*(\d+(?:\.\d+)?)' +ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' +vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' +hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' +sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' +lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' +dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' +tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' +tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' +tieng_duc = 'Tiếng Đức:\s*(\d+(?:\.\d+)?)' +tieng_nhat = 'Tiếng Nhật:\s*(\d+(?:\.\d+)?)' +tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' + +[insert] +sql = """ +INSERT OR REPLACE INTO student + (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, ten_cum_thi, gioi_tinh, + toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, + lich_su, dia_ly, + tieng_anh, tieng_phap, tieng_duc, tieng_nhat, tieng_trung) +VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +""" diff --git a/2016/tools/xlsxread/configs/thptqg2017-data-old.toml b/2016/tools/xlsxread/configs/thptqg2017-data-old.toml new file mode 100644 index 0000000..34d59c0 --- /dev/null +++ b/2016/tools/xlsxread/configs/thptqg2017-data-old.toml @@ -0,0 +1,76 @@ +# Test-only config used by golden integration tests (data-old variant). +# Same column layout as thptqg2017-data.toml but with: +# - sheet_mode = "first" (reads only first sheet) +# - require_numeric_sbd = true (rejects non-digit SBDs) +# Schema and INSERT match SCORE_FIELDS_2017 (14 score cols). + +[reader] +sheet_mode = "first" +strip_blank_rows = false + +[columns] +ho_ten = 0 +ngay_sinh = 1 +so_bao_danh = 2 +diem_thi = 3 + +[validation] +require_numeric_sbd = true +require_nonempty_name = true +require_nonempty_sbd = true + +[header] +tokens = ["HO_TEN", "HỌ TÊN", "STT"] + +[schema] +ddl = """ +CREATE TABLE student ( + so_bao_danh TEXT PRIMARY KEY, + ho_ten TEXT NOT NULL, + ho_ten_ascii TEXT NOT NULL, + ngay_sinh TEXT, + toan REAL, + ngu_van REAL, + vat_ly REAL, + hoa_hoc REAL, + sinh_hoc REAL, + khtn REAL, + lich_su REAL, + dia_ly REAL, + gdcd REAL, + khxh REAL, + tieng_anh REAL, + tieng_phap REAL, + tieng_nga REAL, + tieng_trung REAL +); +CREATE INDEX idx_ho_ten ON student(ho_ten); +CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); +""" + +[scores] +toan = 'Toán:\s*(\d+(?:\.\d+)?)' +ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' +vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' +hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' +sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' +khtn = 'KHTN:\s*(\d+(?:\.\d+)?)' +lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' +dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' +gdcd = 'GDCD:\s*(\d+(?:\.\d+)?)' +khxh = 'KHXH:\s*(\d+(?:\.\d+)?)' +tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' +tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' +tieng_nga = 'Tiếng Nga:\s*(\d+(?:\.\d+)?)' +tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' + +[insert] +sql = """ +INSERT OR REPLACE INTO student + (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, + toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, + lich_su, dia_ly, gdcd, khxh, + tieng_anh, tieng_phap, tieng_nga, tieng_trung) +VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +""" diff --git a/2016/tools/xlsxread/configs/thptqg2017-data.toml b/2016/tools/xlsxread/configs/thptqg2017-data.toml new file mode 100644 index 0000000..892fc6f --- /dev/null +++ b/2016/tools/xlsxread/configs/thptqg2017-data.toml @@ -0,0 +1,76 @@ +# Test-only config used by golden integration tests. +# Matches the fixture row layout produced by tests/golden.rs: write_xlsx() +# header: HO_TEN(0) NGAY_SINH(1) SO_BAO_DANH(2) DIEM_THI(3) +# Schema and INSERT match SCORE_FIELDS_2017 (14 score cols) so run_build_cmd +# in golden.rs can call insert_row(..., SCORE_FIELDS) without modification. + +[reader] +sheet_mode = "all" +strip_blank_rows = false + +[columns] +ho_ten = 0 +ngay_sinh = 1 +so_bao_danh = 2 +diem_thi = 3 + +[validation] +require_numeric_sbd = false +require_nonempty_name = true +require_nonempty_sbd = true + +[header] +tokens = ["HO_TEN", "HỌ TÊN", "STT"] + +[schema] +ddl = """ +CREATE TABLE student ( + so_bao_danh TEXT PRIMARY KEY, + ho_ten TEXT NOT NULL, + ho_ten_ascii TEXT NOT NULL, + ngay_sinh TEXT, + toan REAL, + ngu_van REAL, + vat_ly REAL, + hoa_hoc REAL, + sinh_hoc REAL, + khtn REAL, + lich_su REAL, + dia_ly REAL, + gdcd REAL, + khxh REAL, + tieng_anh REAL, + tieng_phap REAL, + tieng_nga REAL, + tieng_trung REAL +); +CREATE INDEX idx_ho_ten ON student(ho_ten); +CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); +""" + +[scores] +toan = 'Toán:\s*(\d+(?:\.\d+)?)' +ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' +vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' +hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' +sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' +khtn = 'KHTN:\s*(\d+(?:\.\d+)?)' +lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' +dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' +gdcd = 'GDCD:\s*(\d+(?:\.\d+)?)' +khxh = 'KHXH:\s*(\d+(?:\.\d+)?)' +tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' +tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' +tieng_nga = 'Tiếng Nga:\s*(\d+(?:\.\d+)?)' +tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' + +[insert] +sql = """ +INSERT OR REPLACE INTO student + (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, + toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, + lich_su, dia_ly, gdcd, khxh, + tieng_anh, tieng_phap, tieng_nga, tieng_trung) +VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +""" diff --git a/parser/src/audit.rs b/2016/tools/xlsxread/src/audit.rs similarity index 100% rename from parser/src/audit.rs rename to 2016/tools/xlsxread/src/audit.rs diff --git a/parser/src/cli.rs b/2016/tools/xlsxread/src/cli.rs similarity index 100% rename from parser/src/cli.rs rename to 2016/tools/xlsxread/src/cli.rs diff --git a/parser/src/config.rs b/2016/tools/xlsxread/src/config.rs similarity index 81% rename from parser/src/config.rs rename to 2016/tools/xlsxread/src/config.rs index c23e090..e2aaa8e 100644 --- a/parser/src/config.rs +++ b/2016/tools/xlsxread/src/config.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs; use std::path::Path; @@ -9,14 +10,7 @@ use crate::error::BuildError; // Top-level dataset configuration loaded from a .toml file // --------------------------------------------------------------------------- -/// Per-dataset parse rules. -/// -/// Deliberately carries no SQL. The table shape, the INSERT and the subject -/// regexes are identical for every dataset and live in `crate::schema` — keeping -/// them here meant four copies of the same DDL, which is how the 2016 and 2017 -/// schemas drifted apart. #[derive(Debug, Deserialize, Clone)] -#[serde(deny_unknown_fields)] pub struct DatasetConfig { pub reader: ReaderCfg, /// Fixed column indices. Optional when format_detection handles per-file mapping. @@ -24,6 +18,10 @@ pub struct DatasetConfig { pub columns: Option, pub validation: ValidationCfg, pub header: HeaderCfg, + pub schema: SchemaCfg, + /// field name → regex source string (one entry per scoreable subject) + pub scores: HashMap, + pub insert: InsertCfg, /// When set to "thptqg2016", enables per-file format auto-detection. /// Each file's header row is inspected at runtime to choose the right /// column layout (separate-scores / mapped / default-positional). @@ -70,6 +68,18 @@ pub struct HeaderCfg { pub tokens: Vec, } +#[derive(Debug, Deserialize, Clone)] +pub struct SchemaCfg { + /// DDL executed verbatim before inserts (CREATE TABLE + CREATE INDEX) + pub ddl: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct InsertCfg { + /// Parameterised INSERT OR REPLACE SQL using :named_param style + pub sql: String, +} + // --------------------------------------------------------------------------- // Loader // --------------------------------------------------------------------------- @@ -109,6 +119,16 @@ require_nonempty_sbd = true [header] tokens = ["HO_TEN", "HỌ TÊN", "STT"] + +[schema] +ddl = "CREATE TABLE student (so_bao_danh TEXT PRIMARY KEY);" + +[scores] +toan = 'Toán:\s*(\d+(?:\.\d+)?)' +ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' + +[insert] +sql = "INSERT OR REPLACE INTO student (so_bao_danh) VALUES (:so_bao_danh)" "#; #[test] @@ -122,20 +142,11 @@ tokens = ["HO_TEN", "HỌ TÊN", "STT"] assert!(!cfg.validation.require_numeric_sbd); assert!(cfg.validation.require_nonempty_name); assert_eq!(cfg.header.tokens.len(), 3); + assert!(cfg.scores.contains_key("toan")); + assert!(cfg.scores.contains_key("ngu_van")); assert!(cfg.format_detection.is_none()); } - /// A config carrying leftover SQL sections must be rejected rather than - /// silently ignored — otherwise a stale [schema] block would look effective - /// while `crate::schema` was actually driving the build. - #[test] - fn config_rejects_leftover_sql_sections() { - let with_ddl = format!( - "{SAMPLE_TOML}\n[schema]\nddl = \"CREATE TABLE student (so_bao_danh TEXT);\"\n" - ); - assert!(toml::from_str::(&with_ddl).is_err()); - } - #[test] fn config_first_sheet_mode() { let toml_str = SAMPLE_TOML.replace(r#"sheet_mode = "all""#, r#"sheet_mode = "first""#); @@ -160,6 +171,15 @@ require_nonempty_sbd = true [header] tokens = ["SBD", "SOBAODANH", "STT"] + +[schema] +ddl = "CREATE TABLE student (so_bao_danh TEXT PRIMARY KEY);" + +[scores] +toan = 'Toán:\s*(\d+(?:\.\d+)?)' + +[insert] +sql = "INSERT OR REPLACE INTO student (so_bao_danh) VALUES (?)" "#; let cfg: DatasetConfig = toml::from_str(toml_str).expect("parse failed"); assert_eq!(cfg.format_detection.as_deref(), Some("thptqg2016")); diff --git a/parser/src/error.rs b/2016/tools/xlsxread/src/error.rs similarity index 100% rename from parser/src/error.rs rename to 2016/tools/xlsxread/src/error.rs diff --git a/parser/src/format_detect_2016.rs b/2016/tools/xlsxread/src/format_detect_2016.rs similarity index 95% rename from parser/src/format_detect_2016.rs rename to 2016/tools/xlsxread/src/format_detect_2016.rs index fd62f8c..36c2bfd 100644 --- a/parser/src/format_detect_2016.rs +++ b/2016/tools/xlsxread/src/format_detect_2016.rs @@ -7,7 +7,7 @@ /// /// 1. `separate-scores` — header row[0]=="SBD" && row[2]=="TOAN" /// Columns: SBD(0) HOTEN(1) TOAN(2) VAN(3) LY(4) HOA(5) SINH(6) SU(7) DIA(8) -/// NGOAINGUTN(9) NGOAINGUTL(10) NGOAINGU-total(11) +/// NGOAINGUTN(9) NGOAINGUTL(10) NGOAINGU-total(11) /// → maps col 11 → tieng_anh; no ngay_sinh / ten_cum_thi / gioi_tinh / DIEM_THI /// → JS: build-database.js:90–116 (processSeparateScoresRow) /// @@ -19,10 +19,7 @@ /// SBD(0) HO_TEN(1) NGAY_SINH(2) TEN_CUMTHI(3) GIOI_TINH(4) DIEM_THI(5) /// → JS: build-database.js:149–151 DEFAULT_MAP + processMappedRow /// -/// The `build-database.js` citations throughout this file refer to the Node -/// script this parser replaced, in the original standalone thptqg2016 repo. -/// That file no longer exists here; the references are kept because they -/// explain why several of the rules below look arbitrary. +/// JS citations are line numbers in /config/workspace/tiennm99/thptqg2016/scripts/build-database.js. use calamine::Data; use crate::transform::{parse_scores, to_ascii, CompiledPatterns, ParsedRow}; @@ -60,7 +57,7 @@ pub fn is_header_row_2016(row: &[Data]) -> bool { return false; } let first = row[0].to_string().trim().to_uppercase(); - KNOWN_HEADERS.contains(&first.as_str()) + KNOWN_HEADERS.iter().any(|h| *h == first.as_str()) } // --------------------------------------------------------------------------- @@ -220,9 +217,6 @@ pub fn process_separate_scores_row(row: &[Data], patterns: &CompiledPatterns) -> /// Mirrors `processMappedRow` at build-database.js:119–146. /// Gender is normalised: only "Nam" or "Nữ" are kept; everything else → None. /// (build-database.js:132: `(rawGioiTinh === "Nam" || rawGioiTinh === "Nữ") ? rawGioiTinh : null`) -// Mirrors the JS column map one-for-one; grouping the indices into a struct -// would obscure that correspondence for no benefit. -#[allow(clippy::too_many_arguments)] pub fn process_mapped_row( row: &[Data], sbd_idx: usize, @@ -243,8 +237,8 @@ pub fn process_mapped_row( // build-database.js:125–126: KNOWN_HEADERS.has(sbdUpper) || KNOWN_HEADERS.has(hoTenUpper) let sbd_upper = sbd.to_uppercase(); let ho_ten_upper = ho_ten.to_uppercase(); - if KNOWN_HEADERS.contains(&sbd_upper.as_str()) - || KNOWN_HEADERS.contains(&ho_ten_upper.as_str()) + if KNOWN_HEADERS.iter().any(|h| *h == sbd_upper.as_str()) + || KNOWN_HEADERS.iter().any(|h| *h == ho_ten_upper.as_str()) { return None; } @@ -343,13 +337,20 @@ pub fn process_row_2016( #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; fn s(v: &str) -> Data { Data::String(v.to_string()) } fn make_patterns() -> CompiledPatterns { - CompiledPatterns::new().unwrap() + let mut map = HashMap::new(); + map.insert("toan".into(), r"Toán:\s*(\d+(?:\.\d+)?)".into()); + map.insert("ngu_van".into(), r"Ngữ văn:\s*(\d+(?:\.\d+)?)".into()); + map.insert("tieng_anh".into(), r"Tiếng Anh:\s*(\d+(?:\.\d+)?)".into()); + map.insert("tieng_duc".into(), r"Tiếng Đức:\s*(\d+(?:\.\d+)?)".into()); + map.insert("tieng_nhat".into(), r"Tiếng Nhật:\s*(\d+(?:\.\d+)?)".into()); + CompiledPatterns::new(&map).unwrap() } // --- detect_format: branch 1 — separate-scores --- diff --git a/parser/src/lib.rs b/2016/tools/xlsxread/src/lib.rs similarity index 95% rename from parser/src/lib.rs rename to 2016/tools/xlsxread/src/lib.rs index ce7f5e4..b61560c 100644 --- a/parser/src/lib.rs +++ b/2016/tools/xlsxread/src/lib.rs @@ -6,6 +6,5 @@ pub mod config; pub mod error; pub mod format_detect_2016; pub mod reader; -pub mod schema; pub mod transform; pub mod writer; diff --git a/parser/src/main.rs b/2016/tools/xlsxread/src/main.rs similarity index 95% rename from parser/src/main.rs rename to 2016/tools/xlsxread/src/main.rs index fd8b342..0736ed7 100644 --- a/parser/src/main.rs +++ b/2016/tools/xlsxread/src/main.rs @@ -25,7 +25,9 @@ use xlsxread::format_detect_2016::{ }; use xlsxread::reader::{is_all_blank, process_file}; use xlsxread::transform::{validate_row, CompiledPatterns, SkipReason}; -use xlsxread::writer::{finish_db, insert_row, open_db}; +use xlsxread::writer::{ + finish_db, insert_row, insert_row_2016, open_db, SCORE_FIELDS, SCORE_FIELDS_2016, +}; fn main() -> Result<()> { let cli = Cli::parse(); @@ -77,7 +79,7 @@ fn run_build_standard( output_path: &Path, ) -> Result<()> { let patterns = - CompiledPatterns::new().with_context(|| "Failed to compile score regexes")?; + CompiledPatterns::new(&cfg.scores).with_context(|| "Failed to compile score regexes")?; let mut files: Vec = std::fs::read_dir(input_dir) .with_context(|| format!("Cannot read input dir: {}", input_dir.display()))? @@ -107,7 +109,7 @@ fn run_build_standard( files.len() ); - let conn = open_db(output_path) + let conn = open_db(output_path, cfg) .with_context(|| format!("Failed to open DB: {}", output_path.display()))?; let mut total_source_rows: u64 = 0; @@ -157,7 +159,7 @@ fn run_build_standard( let parsed = xlsxread::transform::transform_row(raw, cfg, &patterns); - match insert_row(&conn, &parsed) { + match insert_row(&conn, &cfg.insert.sql, &parsed, SCORE_FIELDS) { Ok(()) => file_rows += 1, Err(e) => { file_errors += 1; @@ -214,7 +216,7 @@ fn run_build_2016( output_path: &Path, ) -> Result<()> { let patterns = - CompiledPatterns::new().with_context(|| "Failed to compile score regexes")?; + CompiledPatterns::new(&cfg.scores).with_context(|| "Failed to compile score regexes")?; let mut files: Vec = std::fs::read_dir(input_dir) .with_context(|| format!("Cannot read input dir: {}", input_dir.display()))? @@ -244,7 +246,7 @@ fn run_build_2016( files.len() ); - let conn = open_db(output_path) + let conn = open_db(output_path, cfg) .with_context(|| format!("Failed to open DB: {}", output_path.display()))?; let mut total_source_rows: u64 = 0; @@ -359,7 +361,7 @@ fn process_file_2016( // Row was empty/invalid — skipped (mirrors JS `if (!record) continue`) } Some(parsed) => { - match insert_row(conn, &parsed) { + match insert_row_2016(conn, &cfg.insert.sql, &parsed, SCORE_FIELDS_2016) { Ok(()) => file_rows += 1, Err(e) => { *total_errors += 1; diff --git a/parser/src/reader.rs b/2016/tools/xlsxread/src/reader.rs similarity index 100% rename from parser/src/reader.rs rename to 2016/tools/xlsxread/src/reader.rs diff --git a/parser/src/transform.rs b/2016/tools/xlsxread/src/transform.rs similarity index 93% rename from parser/src/transform.rs rename to 2016/tools/xlsxread/src/transform.rs index c228f41..c688be4 100644 --- a/parser/src/transform.rs +++ b/2016/tools/xlsxread/src/transform.rs @@ -15,25 +15,22 @@ use crate::error::BuildError; // --------------------------------------------------------------------------- pub struct CompiledPatterns { - /// One compiled regex per subject column in `schema::SCORE_PATTERNS`. + /// Ordered list so INSERT column order is deterministic pub patterns: Vec<(String, Regex)>, } impl CompiledPatterns { - /// Compile the canonical subject patterns once at startup. - /// - /// All 16 patterns run against every dataset. A subject that did not exist - /// in a given exam year simply never matches and stays NULL — the parity - /// check asserts those counts are exactly zero rather than assuming it. - pub fn new() -> Result { - let mut patterns = Vec::with_capacity(crate::schema::SCORE_PATTERNS.len()); - for (field, src) in crate::schema::SCORE_PATTERNS { + pub fn new(scores: &HashMap) -> Result { + let mut patterns = Vec::with_capacity(scores.len()); + for (field, src) in scores { let re = Regex::new(src).map_err(|e| BuildError::Regex { - pattern: (*src).to_string(), + pattern: src.clone(), source: e, })?; - patterns.push(((*field).to_string(), re)); + patterns.push((field.clone(), re)); } + // Sort for deterministic order across HashMap iteration + patterns.sort_by(|a, b| a.0.cmp(&b.0)); Ok(Self { patterns }) } } @@ -317,7 +314,11 @@ mod tests { // --- Score parsing tests --- fn make_patterns() -> CompiledPatterns { - CompiledPatterns::new().unwrap() + let mut map = HashMap::new(); + map.insert("toan".into(), r"Toán:\s*(\d+(?:\.\d+)?)".into()); + map.insert("ngu_van".into(), r"Ngữ văn:\s*(\d+(?:\.\d+)?)".into()); + map.insert("vat_ly".into(), r"Vật lí:\s*(\d+(?:\.\d+)?)".into()); + CompiledPatterns::new(&map).unwrap() } #[test] @@ -326,7 +327,7 @@ mod tests { let s = "Toán: 8.5"; let scores = parse_scores(s, &p); assert_eq!(scores.get("toan"), Some(&8.5)); - assert!(!scores.contains_key("ngu_van")); + assert!(scores.get("ngu_van").is_none()); } #[test] diff --git a/2016/tools/xlsxread/src/writer.rs b/2016/tools/xlsxread/src/writer.rs new file mode 100644 index 0000000..07a83f0 --- /dev/null +++ b/2016/tools/xlsxread/src/writer.rs @@ -0,0 +1,201 @@ +/// SQLite writer: DDL setup, batched INSERT OR REPLACE, VACUUM, stats output. +/// +/// Mirrors build-lib.js createDb + the transaction loop in each build-database*.js. +/// Stats output lines match the JS stdout exactly so existing CI log-greps still work. +/// +/// thptqg2016 differences vs thptqg2017: +/// - INSERT includes ten_cum_thi and gioi_tinh after ngay_sinh +/// - Score columns are toan/ngu_van/vat_ly/hoa_hoc/sinh_hoc/lich_su/dia_ly/ +/// tieng_anh/tieng_phap/tieng_duc/tieng_nhat/tieng_trung +/// (no khtn, khxh, tieng_nga) +use std::fs; +use std::path::Path; + +use rusqlite::{params_from_iter, Connection, ToSql}; + +use crate::config::DatasetConfig; +use crate::error::BuildError; +use crate::transform::ParsedRow; + +// --------------------------------------------------------------------------- +// DB initialisation — mirrors build-lib.js createDb (delete + recreate) +// --------------------------------------------------------------------------- + +/// Open (or recreate) the output SQLite database, execute the DDL from config, +/// and return the open connection ready for inserts. +pub fn open_db(db_path: &Path, cfg: &DatasetConfig) -> Result { + // Mirror Node behaviour: delete existing file before creating (build-lib.js:54) + if db_path.exists() { + fs::remove_file(db_path).map_err(|e| BuildError::Io { + path: db_path.display().to_string(), + source: e, + })?; + } + + // Ensure parent directory exists + if let Some(parent) = db_path.parent() { + if !parent.as_os_str().is_empty() { + fs::create_dir_all(parent).map_err(|e| BuildError::Io { + path: parent.display().to_string(), + source: e, + })?; + } + } + + let conn = Connection::open(db_path)?; + conn.execute_batch(&cfg.schema.ddl)?; + Ok(conn) +} + +// --------------------------------------------------------------------------- +// Score field lists — one per dataset schema +// --------------------------------------------------------------------------- + +/// thptqg2017 score columns (14 fields including khtn/khxh/tieng_nga). +pub const SCORE_FIELDS_2017: &[&str] = &[ + "toan", + "ngu_van", + "vat_ly", + "hoa_hoc", + "sinh_hoc", + "khtn", + "lich_su", + "dia_ly", + "gdcd", + "khxh", + "tieng_anh", + "tieng_phap", + "tieng_nga", + "tieng_trung", +]; + +/// thptqg2016 score columns (12 fields: tieng_duc/tieng_nhat present; no khtn/khxh/tieng_nga). +pub const SCORE_FIELDS_2016: &[&str] = &[ + "toan", + "ngu_van", + "vat_ly", + "hoa_hoc", + "sinh_hoc", + "lich_su", + "dia_ly", + "tieng_anh", + "tieng_phap", + "tieng_duc", + "tieng_nhat", + "tieng_trung", +]; + +/// Alias kept so thptqg2017 callers that import SCORE_FIELDS continue to compile. +pub const SCORE_FIELDS: &[&str] = SCORE_FIELDS_2017; + +// --------------------------------------------------------------------------- +// Insert a single parsed row (thptqg2017 schema — no ten_cum_thi / gioi_tinh) +// --------------------------------------------------------------------------- + +/// Bind all fields from `row` into the prepared statement and execute it. +/// Positional params: so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, +pub fn insert_row( + conn: &Connection, + sql: &str, + row: &ParsedRow, + score_fields: &[&str], +) -> Result<(), BuildError> { + let mut params: Vec> = Vec::with_capacity(4 + score_fields.len()); + params.push(Box::new(row.so_bao_danh.clone())); + params.push(Box::new(row.ho_ten.clone())); + params.push(Box::new(row.ho_ten_ascii.clone())); + params.push(Box::new(row.ngay_sinh.clone())); + + for field in score_fields { + let val: Option = row.scores.get(*field).copied(); + params.push(Box::new(val)); + } + + conn.execute(sql, params_from_iter(params.iter().map(|p| p.as_ref())))?; + Ok(()) +} + +// --------------------------------------------------------------------------- +// Insert a single parsed row (thptqg2016 schema — includes ten_cum_thi + gioi_tinh) +// --------------------------------------------------------------------------- + +/// Bind all fields from `row` into the prepared statement for the thptqg2016 schema. +/// Positional params: so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, ten_cum_thi, +/// gioi_tinh, +pub fn insert_row_2016( + conn: &Connection, + sql: &str, + row: &ParsedRow, + score_fields: &[&str], +) -> Result<(), BuildError> { + let mut params: Vec> = Vec::with_capacity(6 + score_fields.len()); + params.push(Box::new(row.so_bao_danh.clone())); + params.push(Box::new(row.ho_ten.clone())); + params.push(Box::new(row.ho_ten_ascii.clone())); + params.push(Box::new(row.ngay_sinh.clone())); + params.push(Box::new(row.ten_cum_thi.clone())); + params.push(Box::new(row.gioi_tinh.clone())); + + for field in score_fields { + let val: Option = row.scores.get(*field).copied(); + params.push(Box::new(val)); + } + + conn.execute(sql, params_from_iter(params.iter().map(|p| p.as_ref())))?; + Ok(()) +} + +// --------------------------------------------------------------------------- +// Post-build: VACUUM + stats output +// --------------------------------------------------------------------------- + +/// Run VACUUM and print statistics lines that mirror the Node scripts' stdout. +/// The exact prefix tokens ("Source data rows", "DB rows", "Size:") are preserved +/// so any log-grep in the deploy pipeline keeps working. +#[allow(clippy::too_many_arguments)] +pub fn finish_db( + conn: &Connection, + db_path: &Path, + source_rows: u64, + skipped: u64, + errors: u64, + dataset_label: &str, + _file_count: usize, + is_old2: bool, +) -> Result<(), BuildError> { + conn.execute_batch("VACUUM")?; + + let db_count: i64 = conn.query_row("SELECT COUNT(*) FROM student", [], |row| row.get(0))?; + + let insertable = source_rows - skipped; + + println!(); + if is_old2 { + println!("Source non-blank data rows: {source_rows}"); + println!(" skipped (empty/non-numeric SBD): {skipped}"); + } else { + println!("Source data rows (post-header): {source_rows}"); + if dataset_label.contains("old") { + println!(" skipped (empty/non-numeric SBD): {skipped}"); + } else { + println!(" skipped (empty/invalid): {skipped}"); + } + } + println!(" insertable: {insertable}"); + println!(" insert errors: {errors}"); + println!("DB rows (distinct SBD): {db_count}"); + + if !dataset_label.contains("old") && errors == 0 { + let gap = insertable as i64 - db_count; + if gap == 0 { + println!("Audit: OK — every source row made it in."); + } else { + println!("Audit: {gap} row(s) collapsed (duplicate SBDs overwriting)."); + } + } + + let sz = fs::metadata(db_path).map(|m| m.len()).unwrap_or(0); + println!("Size: {:.1} MB", sz as f64 / 1024.0 / 1024.0); + + Ok(()) +} diff --git a/parser/tests/fixtures/README.md b/2016/tools/xlsxread/tests/fixtures/README.md similarity index 100% rename from parser/tests/fixtures/README.md rename to 2016/tools/xlsxread/tests/fixtures/README.md diff --git a/parser/tests/fixtures/hcm-overflow.xlsx b/2016/tools/xlsxread/tests/fixtures/hcm-overflow.xlsx similarity index 100% rename from parser/tests/fixtures/hcm-overflow.xlsx rename to 2016/tools/xlsxread/tests/fixtures/hcm-overflow.xlsx diff --git a/parser/tests/fixtures/province-100.xlsx b/2016/tools/xlsxread/tests/fixtures/province-100.xlsx similarity index 100% rename from parser/tests/fixtures/province-100.xlsx rename to 2016/tools/xlsxread/tests/fixtures/province-100.xlsx diff --git a/parser/tests/fixtures/province-numeric-sbd.xlsx b/2016/tools/xlsxread/tests/fixtures/province-numeric-sbd.xlsx similarity index 100% rename from parser/tests/fixtures/province-numeric-sbd.xlsx rename to 2016/tools/xlsxread/tests/fixtures/province-numeric-sbd.xlsx diff --git a/parser/tests/golden.rs b/2016/tools/xlsxread/tests/golden.rs similarity index 81% rename from parser/tests/golden.rs rename to 2016/tools/xlsxread/tests/golden.rs index b43437c..0038945 100644 --- a/parser/tests/golden.rs +++ b/2016/tools/xlsxread/tests/golden.rs @@ -1,7 +1,9 @@ -/// Golden tests — integration tests using anonymised fixture files. +/// Stage 5 golden tests — integration tests using anonymised fixture files. /// /// Fixture files are generated in-process via raw OOXML + zip if they do not -/// already exist on disk. No external tooling is required. +/// already exist on disk. No external Python or Node tooling required for the +/// Rust-side tests. The Node golden comparison is marked #[ignore] when pnpm +/// is not in PATH. use std::io::Write as IoWrite; use std::path::{Path, PathBuf}; @@ -123,7 +125,7 @@ fn write_xlsx(path: &Path, sheets: &[(String, Vec)]) { ws.push_str(" \n"); } ws.push_str(" \n"); - zip.start_file(format!("xl/worksheets/sheet{}.xml", i + 1), opts) + zip.start_file(&format!("xl/worksheets/sheet{}.xml", i + 1), opts) .unwrap(); zip.write_all(ws.as_bytes()).unwrap(); } @@ -163,7 +165,7 @@ fn header_row() -> XlsxRow { fn data_row(idx: usize, scores: &str) -> XlsxRow { // Anonymised: name uses sequential pattern, SBD is purely synthetic - let name = if idx.is_multiple_of(2) { + let name = if idx % 2 == 0 { format!("Nguyen Van Test {:03}", idx) } else { format!("Tran Thi Test {:03}", idx) @@ -267,7 +269,7 @@ fn ensure_fixtures() { fn make_data_config() -> xlsxread::config::DatasetConfig { let cfg_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("configs") - .join("2017.toml"); + .join("thptqg2017-data.toml"); xlsxread::config::load_config(&cfg_path).expect("load data config") } @@ -284,7 +286,7 @@ fn province_100_builds_100_rows() { std::fs::create_dir_all(&fixture_dir).unwrap(); std::fs::copy(province_fixture_path(), fixture_dir.join("province.xlsx")).unwrap(); - run_build_cmd(&fixture_dir, &db_path, "2017.toml"); + run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); let count = query_count(&db_path); assert_eq!(count, 100, "expected 100 rows from province-100 fixture"); @@ -299,7 +301,7 @@ fn hcm_overflow_builds_400_rows() { std::fs::create_dir_all(&fixture_dir).unwrap(); std::fs::copy(hcm_overflow_fixture_path(), fixture_dir.join("hcm.xlsx")).unwrap(); - run_build_cmd(&fixture_dir, &db_path, "2017.toml"); + run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); let count = query_count(&db_path); assert_eq!( @@ -318,7 +320,7 @@ fn data_old_first_sheet_only_100_rows() { // Use the overflow file but with data-old config (first sheet only → 200 rows) std::fs::copy(hcm_overflow_fixture_path(), fixture_dir.join("hcm.xlsx")).unwrap(); - run_build_cmd(&fixture_dir, &db_path, "2017-old.toml"); + run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data-old.toml"); // data-old: sheet_mode=first → only 200 rows from sheet1; but SBDs "1000NNNN" are // all digits so all pass the numeric guard @@ -356,7 +358,7 @@ fn numeric_sbd_guard_rejects_non_numeric() { let mixed_path = fixture_dir.join("mixed.xlsx"); write_xlsx(&mixed_path, &[("Sheet1".to_owned(), rows)]); - run_build_cmd(&fixture_dir, &db_path, "2017-old.toml"); + run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data-old.toml"); // Row i=5 has non-numeric SBD → rejected by data-old config let count = query_count(&db_path); @@ -390,7 +392,7 @@ fn scores_parsed_correctly_into_db() { &[("Sheet1".to_owned(), rows)], ); - run_build_cmd(&fixture_dir, &db_path, "2017.toml"); + run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); let conn = rusqlite::Connection::open(&db_path).unwrap(); let (toan, van, anh): (f64, f64, f64) = conn @@ -429,7 +431,7 @@ fn to_ascii_stored_correctly() { &[("Sheet1".to_owned(), rows)], ); - run_build_cmd(&fixture_dir, &db_path, "2017.toml"); + run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); let conn = rusqlite::Connection::open(&db_path).unwrap(); let ascii: String = conn @@ -451,7 +453,7 @@ fn audit_subcommand_matches_after_build() { std::fs::create_dir_all(&fixture_dir).unwrap(); std::fs::copy(province_fixture_path(), fixture_dir.join("province.xlsx")).unwrap(); - run_build_cmd(&fixture_dir, &db_path, "2017.toml"); + run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); // audit should match (100 distinct SBDs in xlsx == 100 rows in DB) let cfg = make_data_config(); @@ -491,7 +493,7 @@ fn audit_subcommand_mismatch_detected() { &[("Sheet1".to_owned(), five_rows)], ); - run_build_cmd(&build_dir, &db_path, "2017.toml"); + run_build_cmd(&build_dir, &db_path, "thptqg2017-data.toml"); // audit against fixture_dir (10 xlsx rows) but DB has 5 rows → mismatch let cfg = make_data_config(); @@ -501,6 +503,60 @@ fn audit_subcommand_mismatch_detected() { assert_eq!(result.db_count, 5); } +// --------------------------------------------------------------------------- +// Golden test: compare Rust DB vs Node DB on identical fixture +// Marked #[ignore] when pnpm / node is not in PATH — CI installs them first. +// --------------------------------------------------------------------------- + +#[test] +#[ignore] +fn golden_rust_matches_node_db() { + // This test requires: pnpm, node, and the thptqg2017 package to be installed + // Run with: cargo test -- --ignored golden_rust_matches_node_db + let which_pnpm = std::process::Command::new("which") + .arg("pnpm") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + if !which_pnpm { + eprintln!("pnpm not in PATH — skipping golden test"); + return; + } + + let dir = tempdir(); + let fixture_dir = dir.join("input"); + std::fs::create_dir_all(&fixture_dir).unwrap(); + std::fs::copy(province_fixture_path(), fixture_dir.join("province.xlsx")).unwrap(); + + // Build with Rust + let rust_db = dir.join("rust.db"); + run_build_cmd(&fixture_dir, &rust_db, "thptqg2017-data.toml"); + + // Build with Node (run build-database.js with DATA_DIR / DB_PATH overrides) + // Node script reads env-vars via a thin wrapper — see scripts/build-database.js + // For now: diff via SELECT * ORDER BY so_bao_danh + let node_db = dir.join("node.db"); + let status = std::process::Command::new("pnpm") + .args(["exec", "node", "scripts/build-database.js"]) + .env("OVERRIDE_SRC_DIR", fixture_dir.to_str().unwrap()) + .env("OVERRIDE_DB_PATH", node_db.to_str().unwrap()) + .current_dir( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap(), + ) + .status() + .expect("failed to run node build script"); + + if !status.success() { + panic!("Node build script failed with: {status}"); + } + + // Row-by-row comparison + diff_dbs(&rust_db, &node_db); +} // --------------------------------------------------------------------------- // Helpers @@ -525,7 +581,8 @@ fn run_build_cmd(input_dir: &Path, db_path: &Path, config_name: &str) { let cfg = xlsxread::config::load_config(&cfg_path) .unwrap_or_else(|e| panic!("load config {config_name}: {e}")); - let patterns = xlsxread::transform::CompiledPatterns::new().expect("compile patterns"); + let patterns = + xlsxread::transform::CompiledPatterns::new(&cfg.scores).expect("compile patterns"); // Collect files let mut files: Vec = std::fs::read_dir(input_dir) @@ -545,7 +602,7 @@ fn run_build_cmd(input_dir: &Path, db_path: &Path, config_name: &str) { .collect(); files.sort(); - let conn = xlsxread::writer::open_db(db_path).expect("open db"); + let conn = xlsxread::writer::open_db(db_path, &cfg).expect("open db"); conn.execute_batch("BEGIN").unwrap(); for file in &files { @@ -575,7 +632,12 @@ fn run_build_cmd(input_dir: &Path, db_path: &Path, config_name: &str) { return; } let row = xlsxread::transform::transform_row(raw, &cfg, &patterns); - let _ = xlsxread::writer::insert_row(&conn, &row); + let _ = xlsxread::writer::insert_row( + &conn, + &cfg.insert.sql, + &row, + xlsxread::writer::SCORE_FIELDS, + ); }) .expect("process file"); } @@ -589,3 +651,41 @@ fn query_count(db_path: &Path) -> i64 { conn.query_row("SELECT COUNT(*) FROM student", [], |r| r.get(0)) .expect("count query") } + +fn diff_dbs(a: &Path, b: &Path) { + let conn_a = rusqlite::Connection::open(a).unwrap(); + + // Attach b as "other" + conn_a + .execute_batch(&format!("ATTACH DATABASE '{}' AS other", b.display())) + .unwrap(); + + // Rows in a not in b + let missing_in_b: i64 = conn_a + .query_row( + "SELECT COUNT(*) FROM main.student s + WHERE NOT EXISTS (SELECT 1 FROM other.student o WHERE o.so_bao_danh = s.so_bao_danh)", + [], + |r| r.get(0), + ) + .unwrap(); + + // Rows in b not in a + let missing_in_a: i64 = conn_a + .query_row( + "SELECT COUNT(*) FROM other.student o + WHERE NOT EXISTS (SELECT 1 FROM main.student s WHERE s.so_bao_danh = o.so_bao_danh)", + [], + |r| r.get(0), + ) + .unwrap(); + + assert_eq!( + missing_in_b, 0, + "{missing_in_b} rows in Rust DB missing from Node DB" + ); + assert_eq!( + missing_in_a, 0, + "{missing_in_a} rows in Node DB missing from Rust DB" + ); +} diff --git a/2016/vite.config.js b/2016/vite.config.js new file mode 100644 index 0000000..e6ac920 --- /dev/null +++ b/2016/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + base: "/thptqg/2016/", +}); diff --git a/2017/.gitignore b/2017/.gitignore new file mode 100644 index 0000000..6f4752f --- /dev/null +++ b/2017/.gitignore @@ -0,0 +1,27 @@ +.idea +*.iws +*.iml +*.ipr + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +### Node ### +node_modules/ + +### Generated databases (all 3 variants) ### +public/thptqg2017.db +public/thptqg2017.db.gz +public-old/thptqg2017.db +public-old/thptqg2017.db.gz +public-old2/thptqg2017.db +public-old2/thptqg2017.db.gz + +### Build output ### +dist/ + +### Rust build artefacts ### +tools/xlsxread/target/ diff --git a/2017/LICENSE b/2017/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/2017/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/2017/README.md b/2017/README.md new file mode 100644 index 0000000..6f9ec13 --- /dev/null +++ b/2017/README.md @@ -0,0 +1,113 @@ +# Vietnam THPT 2017 Score Lookup + +Static React + SQLite site for looking up Vietnamese high school graduation exam scores (2017). The full database (~861k students, 63 provinces) ships to the browser as a compressed SQLite file and queries run client-side via `sql.js` — no backend. + +## Live sites + +Three deployments, one per dataset: + +| URL | Data source | Rows | +|---|---|---| +| `/thptqg2017/` | `data/` — [baotintuc.vn](https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm) CDN, `.xls` | **861,068** | +| `/thptqg2017/old/` | `data-old/` — earlier `.xlsx` collection from Vietnamese news sites at the time (exact origin no longer known) | 847,348 | +| `/thptqg2017/old2/` | `data-old2/` — partial re-export (54 provinces) also from contemporary news sites, exact origin unrecorded | 679,764 | + +## Features + +- Diacritics-insensitive name search (`"nguyen"` matches `"Nguyễn"`) +- Live debounced search with URL deep-link (`?q=49008235`) +- Single-result detail card with per-subject TFT rarity-tiered scores (6 tiers, ≤1 → 9-10) and all 49 admission blocks (A00 – D15) +- Share button: copies a formatted summary plus deep-link URL +- SQL query tab with grouped presets (rankings, Long An filters, statistics, schema) +- Light + dark mode (follows OS preference) +- Keyboard shortcut `/` to focus search + +## Requirements + +- Node.js 24+ +- pnpm +- Rust (stable) — installed via [rustup](https://rustup.rs/); required for `build:db*` scripts + +## Quickstart + +```bash +pnpm install +pnpm build:db # compile xlsxread + parse data/ → public/thptqg2017.db (~2 min, 159 MB) +gzip -kf -9 public/thptqg2017.db +pnpm dev # http://localhost:5173 +``` + +The database build pipeline uses the `xlsxread` Rust CLI located at +`tools/xlsxread/`. It reads `.xls`/`.xlsx` source files, strips +diacritics, parses score text, and writes a SQLite database — replacing +the former Node.js + `xlsx` (SheetJS) pipeline. See +`tools/xlsxread/README.md` for CLI invocation details and config schema. + +## Scripts + +| Command | Action | +|---|---| +| `pnpm dev` | Vite dev server | +| `pnpm build` | Production build (main variant → `dist/`) | +| `pnpm build:old` / `build:old2` | Build variant sites to `dist/old/`, `dist/old2/` | +| `pnpm build:all` | All 3 web variants | +| `pnpm build:rust` | Compile `tools/xlsxread` release binary (run once; auto-called by `build:db*`) | +| `pnpm build:db` | Build main DB from `data/` via xlsxread | +| `pnpm build:db:old` / `build:db:old2` | Build old / old2 variant DBs via xlsxread | +| `pnpm build:db:all` | All 3 DBs via xlsxread | +| `pnpm lint` | ESLint | +| `node scripts/crawl-baotintuc.js` | Re-download all 63 province files from baotintuc.vn | +| `node scripts/check-duplicates.js` | MD5 + row-content duplicate audit | +| `node scripts/diff-datasets.js` | Compare `public/` vs `backup/` DB (when backup present) | + +## Project layout + +``` +. +├── data/ # 63 .xls files (source) +├── data-old/ # 63 .xlsx (previous export) +├── data-old2/ # 54 .xlsx (update/ overrides) +├── public/ # main variant assets + thptqg2017.db.gz +├── public-old/ # old variant assets +├── public-old2/ # old2 variant assets +├── scripts/ +│ ├── crawl-baotintuc.js # downloader +│ ├── check-duplicates.js # md5 dup detector +│ └── diff-datasets.js # DB-to-DB comparator +├── src/ +│ ├── App.jsx +│ ├── App.css +│ ├── components/{search-form, score-table, student-detail, custom-query}.jsx +│ ├── hooks/use-sqlite.js +│ └── lib/admission-blocks.js # 49 admission-block definitions + score-tier helper +├── tools/ +│ └── xlsxread/ # Rust CLI — reads .xls/.xlsx, writes SQLite +│ ├── configs/ +│ │ ├── thptqg2017-data.toml # config for data/ (63 .xls, all sheets) +│ │ ├── thptqg2017-data-old.toml # config for data-old/ (63 .xlsx, sheet 0) +│ │ └── thptqg2017-data-old2.toml # config for data-old2/ (54 .xlsx, all sheets) +│ ├── src/ # Rust source +│ └── README.md # CLI reference + config schema +├── docs/ # see docs/README.md +├── index.html +├── vite.config.js +└── package.json +``` + +See `docs/` for architecture + deployment details. + +## License + +See `LICENSE`. + +## Source + +- **`data/`** — scraped in full from the baotintuc.vn 2017 announcement article: + `https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm` + Reproducible via `node scripts/crawl-baotintuc.js`. +- **`data-old/`** and **`data-old2/`** — collected from Vietnamese news sites at the + time of the 2017 exam. The specific publisher URLs were not recorded and cannot + be recovered now. These datasets are preserved for comparison and historical + reference only; `data/` is the canonical source for the main deployment. + +Intended for reference only. diff --git a/data/2017-old/10_BinhThuan_RIIW.xls.xlsx b/2017/data-old/10_BinhThuan_RIIW.xls.xlsx similarity index 100% rename from data/2017-old/10_BinhThuan_RIIW.xls.xlsx rename to 2017/data-old/10_BinhThuan_RIIW.xls.xlsx diff --git a/data/2017-old/10_Ca_Mau_BKXT.xls.xlsx b/2017/data-old/10_Ca_Mau_BKXT.xls.xlsx similarity index 100% rename from data/2017-old/10_Ca_Mau_BKXT.xls.xlsx rename to 2017/data-old/10_Ca_Mau_BKXT.xls.xlsx diff --git a/data/2017-old/10_LamDong_GNFT.xls.xlsx b/2017/data-old/10_LamDong_GNFT.xls.xlsx similarity index 100% rename from data/2017-old/10_LamDong_GNFT.xls.xlsx rename to 2017/data-old/10_LamDong_GNFT.xls.xlsx diff --git a/data/2017-old/10_Soc_Trang_XCGJ.xls.xlsx b/2017/data-old/10_Soc_Trang_XCGJ.xls.xlsx similarity index 100% rename from data/2017-old/10_Soc_Trang_XCGJ.xls.xlsx rename to 2017/data-old/10_Soc_Trang_XCGJ.xls.xlsx diff --git a/data/2017-old/11_BinhDuong_RYQL.xls.xlsx b/2017/data-old/11_BinhDuong_RYQL.xls.xlsx similarity index 100% rename from data/2017-old/11_BinhDuong_RYQL.xls.xlsx rename to 2017/data-old/11_BinhDuong_RYQL.xls.xlsx diff --git a/data/2017-old/11_LaoCai_GMSU.xls.xlsx b/2017/data-old/11_LaoCai_GMSU.xls.xlsx similarity index 100% rename from data/2017-old/11_LaoCai_GMSU.xls.xlsx rename to 2017/data-old/11_LaoCai_GMSU.xls.xlsx diff --git a/data/2017-old/12_BenTre_DKWF.xls.xlsx b/2017/data-old/12_BenTre_DKWF.xls.xlsx similarity index 100% rename from data/2017-old/12_BenTre_DKWF.xls.xlsx rename to 2017/data-old/12_BenTre_DKWF.xls.xlsx diff --git a/data/2017-old/12_LongAn_ZZUK.xls.xlsx b/2017/data-old/12_LongAn_ZZUK.xls.xlsx similarity index 100% rename from data/2017-old/12_LongAn_ZZUK.xls.xlsx rename to 2017/data-old/12_LongAn_ZZUK.xls.xlsx diff --git a/data/2017-old/13_NamDinh_ESEL.xls.xlsx b/2017/data-old/13_NamDinh_ESEL.xls.xlsx similarity index 100% rename from data/2017-old/13_NamDinh_ESEL.xls.xlsx rename to 2017/data-old/13_NamDinh_ESEL.xls.xlsx diff --git a/data/2017-old/13_TraVinh_LKUJ.xls.xlsx b/2017/data-old/13_TraVinh_LKUJ.xls.xlsx similarity index 100% rename from data/2017-old/13_TraVinh_LKUJ.xls.xlsx rename to 2017/data-old/13_TraVinh_LKUJ.xls.xlsx diff --git a/data/2017-old/14_NgheAn_BSLY.xls.xlsx b/2017/data-old/14_NgheAn_BSLY.xls.xlsx similarity index 100% rename from data/2017-old/14_NgheAn_BSLY.xls.xlsx rename to 2017/data-old/14_NgheAn_BSLY.xls.xlsx diff --git a/data/2017-old/15_PhuTho_ABWQ.xls.xlsx b/2017/data-old/15_PhuTho_ABWQ.xls.xlsx similarity index 100% rename from data/2017-old/15_PhuTho_ABWQ.xls.xlsx rename to 2017/data-old/15_PhuTho_ABWQ.xls.xlsx diff --git a/data/2017-old/16_QuangBinh_KGEU.xls.xlsx b/2017/data-old/16_QuangBinh_KGEU.xls.xlsx similarity index 100% rename from data/2017-old/16_QuangBinh_KGEU.xls.xlsx rename to 2017/data-old/16_QuangBinh_KGEU.xls.xlsx diff --git a/data/2017-old/17_QuangNam_AMTK.xls.xlsx b/2017/data-old/17_QuangNam_AMTK.xls.xlsx similarity index 100% rename from data/2017-old/17_QuangNam_AMTK.xls.xlsx rename to 2017/data-old/17_QuangNam_AMTK.xls.xlsx diff --git a/data/2017-old/18_QuangNgai_KOFP.xls.xlsx b/2017/data-old/18_QuangNgai_KOFP.xls.xlsx similarity index 100% rename from data/2017-old/18_QuangNgai_KOFP.xls.xlsx rename to 2017/data-old/18_QuangNgai_KOFP.xls.xlsx diff --git a/data/2017-old/19_QuangTri_OMZF.xls.xlsx b/2017/data-old/19_QuangTri_OMZF.xls.xlsx similarity index 100% rename from data/2017-old/19_QuangTri_OMZF.xls.xlsx rename to 2017/data-old/19_QuangTri_OMZF.xls.xlsx diff --git a/data/2017-old/1_BaRia_VungTau_HJKG.xls.xlsx b/2017/data-old/1_BaRia_VungTau_HJKG.xls.xlsx similarity index 100% rename from data/2017-old/1_BaRia_VungTau_HJKG.xls.xlsx rename to 2017/data-old/1_BaRia_VungTau_HJKG.xls.xlsx diff --git a/data/2017-old/1_Da_Nang_AHWJ.xls.xlsx b/2017/data-old/1_Da_Nang_AHWJ.xls.xlsx similarity index 100% rename from data/2017-old/1_Da_Nang_AHWJ.xls.xlsx rename to 2017/data-old/1_Da_Nang_AHWJ.xls.xlsx diff --git a/data/2017-old/1_Ha_Noi_CVXG.xls.xlsx b/2017/data-old/1_Ha_Noi_CVXG.xls.xlsx similarity index 100% rename from data/2017-old/1_Ha_Noi_CVXG.xls.xlsx rename to 2017/data-old/1_Ha_Noi_CVXG.xls.xlsx diff --git a/data/2017-old/1_Son_La_JIDP.xls.xlsx b/2017/data-old/1_Son_La_JIDP.xls.xlsx similarity index 100% rename from data/2017-old/1_Son_La_JIDP.xls.xlsx rename to 2017/data-old/1_Son_La_JIDP.xls.xlsx diff --git a/data/2017-old/1_TuyenQuang_JBYF.xls.xlsx b/2017/data-old/1_TuyenQuang_JBYF.xls.xlsx similarity index 100% rename from data/2017-old/1_TuyenQuang_JBYF.xls.xlsx rename to 2017/data-old/1_TuyenQuang_JBYF.xls.xlsx diff --git a/data/2017-old/20_TayNinh_ILFA.xls.xlsx b/2017/data-old/20_TayNinh_ILFA.xls.xlsx similarity index 100% rename from data/2017-old/20_TayNinh_ILFA.xls.xlsx rename to 2017/data-old/20_TayNinh_ILFA.xls.xlsx diff --git a/data/2017-old/21_ThaiBinh_FTVG.xls.xlsx b/2017/data-old/21_ThaiBinh_FTVG.xls.xlsx similarity index 100% rename from data/2017-old/21_ThaiBinh_FTVG.xls.xlsx rename to 2017/data-old/21_ThaiBinh_FTVG.xls.xlsx diff --git a/data/2017-old/22_ThaiNguyen_TLTW.xls.xlsx b/2017/data-old/22_ThaiNguyen_TLTW.xls.xlsx similarity index 100% rename from data/2017-old/22_ThaiNguyen_TLTW.xls.xlsx rename to 2017/data-old/22_ThaiNguyen_TLTW.xls.xlsx diff --git a/data/2017-old/23_HaiPhong_HXBV.xls.xlsx b/2017/data-old/23_HaiPhong_HXBV.xls.xlsx similarity index 100% rename from data/2017-old/23_HaiPhong_HXBV.xls.xlsx rename to 2017/data-old/23_HaiPhong_HXBV.xls.xlsx diff --git a/data/2017-old/24_HCM_XULN.xls.xlsx b/2017/data-old/24_HCM_XULN.xls.xlsx similarity index 100% rename from data/2017-old/24_HCM_XULN.xls.xlsx rename to 2017/data-old/24_HCM_XULN.xls.xlsx diff --git a/data/2017-old/2_BacKan_GFVQ.xls.xlsx b/2017/data-old/2_BacKan_GFVQ.xls.xlsx similarity index 100% rename from data/2017-old/2_BacKan_GFVQ.xls.xlsx rename to 2017/data-old/2_BacKan_GFVQ.xls.xlsx diff --git a/data/2017-old/2_Ha_Giang_QNCM.xls.xlsx b/2017/data-old/2_Ha_Giang_QNCM.xls.xlsx similarity index 100% rename from data/2017-old/2_Ha_Giang_QNCM.xls.xlsx rename to 2017/data-old/2_Ha_Giang_QNCM.xls.xlsx diff --git a/data/2017-old/2_Ninh_Thuan_VHLY.xls.xlsx b/2017/data-old/2_Ninh_Thuan_VHLY.xls.xlsx similarity index 100% rename from data/2017-old/2_Ninh_Thuan_VHLY.xls.xlsx rename to 2017/data-old/2_Ninh_Thuan_VHLY.xls.xlsx diff --git a/data/2017-old/2_Thanh_Hoa_AUFV.xls.xlsx b/2017/data-old/2_Thanh_Hoa_AUFV.xls.xlsx similarity index 100% rename from data/2017-old/2_Thanh_Hoa_AUFV.xls.xlsx rename to 2017/data-old/2_Thanh_Hoa_AUFV.xls.xlsx diff --git a/data/2017-old/2_VinhPhuc_GUDK.xls.xlsx b/2017/data-old/2_VinhPhuc_GUDK.xls.xlsx similarity index 100% rename from data/2017-old/2_VinhPhuc_GUDK.xls.xlsx rename to 2017/data-old/2_VinhPhuc_GUDK.xls.xlsx diff --git a/data/2017-old/3_BacGiang_TOIF.xls.xlsx b/2017/data-old/3_BacGiang_TOIF.xls.xlsx similarity index 100% rename from data/2017-old/3_BacGiang_TOIF.xls.xlsx rename to 2017/data-old/3_BacGiang_TOIF.xls.xlsx diff --git a/data/2017-old/3_BinhPhuoc_YFMU.xls.xlsx b/2017/data-old/3_BinhPhuoc_YFMU.xls.xlsx similarity index 100% rename from data/2017-old/3_BinhPhuoc_YFMU.xls.xlsx rename to 2017/data-old/3_BinhPhuoc_YFMU.xls.xlsx diff --git a/data/2017-old/3_Cao_Bang_CIEY.xls.xlsx b/2017/data-old/3_Cao_Bang_CIEY.xls.xlsx similarity index 100% rename from data/2017-old/3_Cao_Bang_CIEY.xls.xlsx rename to 2017/data-old/3_Cao_Bang_CIEY.xls.xlsx diff --git a/data/2017-old/3_Dong_Thap_GSXQ.xls.xlsx b/2017/data-old/3_Dong_Thap_GSXQ.xls.xlsx similarity index 100% rename from data/2017-old/3_Dong_Thap_GSXQ.xls.xlsx rename to 2017/data-old/3_Dong_Thap_GSXQ.xls.xlsx diff --git a/data/2017-old/3_Thua_Thien_Hue_HMDB.xls.xlsx b/2017/data-old/3_Thua_Thien_Hue_HMDB.xls.xlsx similarity index 100% rename from data/2017-old/3_Thua_Thien_Hue_HMDB.xls.xlsx rename to 2017/data-old/3_Thua_Thien_Hue_HMDB.xls.xlsx diff --git a/data/2017-old/4_An_Giang_JNOS.xls.xlsx b/2017/data-old/4_An_Giang_JNOS.xls.xlsx similarity index 100% rename from data/2017-old/4_An_Giang_JNOS.xls.xlsx rename to 2017/data-old/4_An_Giang_JNOS.xls.xlsx diff --git a/data/2017-old/4_BacNinh_STLR.xls.xlsx b/2017/data-old/4_BacNinh_STLR.xls.xlsx similarity index 100% rename from data/2017-old/4_BacNinh_STLR.xls.xlsx rename to 2017/data-old/4_BacNinh_STLR.xls.xlsx diff --git a/data/2017-old/4_Binh_Dinh_WWZW.xls.xlsx b/2017/data-old/4_Binh_Dinh_WWZW.xls.xlsx similarity index 100% rename from data/2017-old/4_Binh_Dinh_WWZW.xls.xlsx rename to 2017/data-old/4_Binh_Dinh_WWZW.xls.xlsx diff --git a/data/2017-old/4_DienBien_SOJG.xls.xlsx b/2017/data-old/4_DienBien_SOJG.xls.xlsx similarity index 100% rename from data/2017-old/4_DienBien_SOJG.xls.xlsx rename to 2017/data-old/4_DienBien_SOJG.xls.xlsx diff --git a/data/2017-old/4_Lang_Son_NYQL.xls.xlsx b/2017/data-old/4_Lang_Son_NYQL.xls.xlsx similarity index 100% rename from data/2017-old/4_Lang_Son_NYQL.xls.xlsx rename to 2017/data-old/4_Lang_Son_NYQL.xls.xlsx diff --git a/data/2017-old/5_Bac_Lieu_XEKH.xls.xlsx b/2017/data-old/5_Bac_Lieu_XEKH.xls.xlsx similarity index 100% rename from data/2017-old/5_Bac_Lieu_XEKH.xls.xlsx rename to 2017/data-old/5_Bac_Lieu_XEKH.xls.xlsx diff --git a/data/2017-old/5_Gia_Lai_ABZI.xls.xlsx b/2017/data-old/5_Gia_Lai_ABZI.xls.xlsx similarity index 100% rename from data/2017-old/5_Gia_Lai_ABZI.xls.xlsx rename to 2017/data-old/5_Gia_Lai_ABZI.xls.xlsx diff --git a/data/2017-old/5_HaiDuong_WWWG.xls.xlsx b/2017/data-old/5_HaiDuong_WWWG.xls.xlsx similarity index 100% rename from data/2017-old/5_HaiDuong_WWWG.xls.xlsx rename to 2017/data-old/5_HaiDuong_WWWG.xls.xlsx diff --git a/data/2017-old/5_Hanam_QGJS.xls.xlsx b/2017/data-old/5_Hanam_QGJS.xls.xlsx similarity index 100% rename from data/2017-old/5_Hanam_QGJS.xls.xlsx rename to 2017/data-old/5_Hanam_QGJS.xls.xlsx diff --git a/data/2017-old/5_Yen_Bai_FAQR.xls.xlsx b/2017/data-old/5_Yen_Bai_FAQR.xls.xlsx similarity index 100% rename from data/2017-old/5_Yen_Bai_FAQR.xls.xlsx rename to 2017/data-old/5_Yen_Bai_FAQR.xls.xlsx diff --git a/data/2017-old/6_Dong_Nai_WOTM.xls.xlsx b/2017/data-old/6_Dong_Nai_WOTM.xls.xlsx similarity index 100% rename from data/2017-old/6_Dong_Nai_WOTM.xls.xlsx rename to 2017/data-old/6_Dong_Nai_WOTM.xls.xlsx diff --git a/data/2017-old/6_Hau_Giang_KWDM.xls.xlsx b/2017/data-old/6_Hau_Giang_KWDM.xls.xlsx similarity index 100% rename from data/2017-old/6_Hau_Giang_KWDM.xls.xlsx rename to 2017/data-old/6_Hau_Giang_KWDM.xls.xlsx diff --git a/data/2017-old/6_HoaBinh_TPZY.xls.xlsx b/2017/data-old/6_HoaBinh_TPZY.xls.xlsx similarity index 100% rename from data/2017-old/6_HoaBinh_TPZY.xls.xlsx rename to 2017/data-old/6_HoaBinh_TPZY.xls.xlsx diff --git a/data/2017-old/6_NinhBinh_IGFT.xls.xlsx b/2017/data-old/6_NinhBinh_IGFT.xls.xlsx similarity index 100% rename from data/2017-old/6_NinhBinh_IGFT.xls.xlsx rename to 2017/data-old/6_NinhBinh_IGFT.xls.xlsx diff --git a/data/2017-old/6_Quang_Ninh_DQCJ.xls.xlsx b/2017/data-old/6_Quang_Ninh_DQCJ.xls.xlsx similarity index 100% rename from data/2017-old/6_Quang_Ninh_DQCJ.xls.xlsx rename to 2017/data-old/6_Quang_Ninh_DQCJ.xls.xlsx diff --git a/data/2017-old/7_HaTinh_DDHD.xls.xlsx b/2017/data-old/7_HaTinh_DDHD.xls.xlsx similarity index 100% rename from data/2017-old/7_HaTinh_DDHD.xls.xlsx rename to 2017/data-old/7_HaTinh_DDHD.xls.xlsx diff --git a/data/2017-old/7_HungYen_LTIK.xls.xlsx b/2017/data-old/7_HungYen_LTIK.xls.xlsx similarity index 100% rename from data/2017-old/7_HungYen_LTIK.xls.xlsx rename to 2017/data-old/7_HungYen_LTIK.xls.xlsx diff --git a/data/2017-old/7_Kon_Tum_RSLR.xls.xlsx b/2017/data-old/7_Kon_Tum_RSLR.xls.xlsx similarity index 100% rename from data/2017-old/7_Kon_Tum_RSLR.xls.xlsx rename to 2017/data-old/7_Kon_Tum_RSLR.xls.xlsx diff --git a/data/2017-old/7_Tien_Giang_EFHX.xls.xlsx b/2017/data-old/7_Tien_Giang_EFHX.xls.xlsx similarity index 100% rename from data/2017-old/7_Tien_Giang_EFHX.xls.xlsx rename to 2017/data-old/7_Tien_Giang_EFHX.xls.xlsx diff --git a/data/2017-old/8_Can_Tho_RQZM.xls.xlsx b/2017/data-old/8_Can_Tho_RQZM.xls.xlsx similarity index 100% rename from data/2017-old/8_Can_Tho_RQZM.xls.xlsx rename to 2017/data-old/8_Can_Tho_RQZM.xls.xlsx diff --git a/data/2017-old/8_Dak_Lak_YKPR.xls.xlsx b/2017/data-old/8_Dak_Lak_YKPR.xls.xlsx similarity index 100% rename from data/2017-old/8_Dak_Lak_YKPR.xls.xlsx rename to 2017/data-old/8_Dak_Lak_YKPR.xls.xlsx diff --git a/data/2017-old/8_KienGiang_OTOB.xls.xlsx b/2017/data-old/8_KienGiang_OTOB.xls.xlsx similarity index 100% rename from data/2017-old/8_KienGiang_OTOB.xls.xlsx rename to 2017/data-old/8_KienGiang_OTOB.xls.xlsx diff --git a/data/2017-old/8_PhuYen_OGIM.xls.xlsx b/2017/data-old/8_PhuYen_OGIM.xls.xlsx similarity index 100% rename from data/2017-old/8_PhuYen_OGIM.xls.xlsx rename to 2017/data-old/8_PhuYen_OGIM.xls.xlsx diff --git a/data/2017-old/9_DakNong_FBOP.xls.xlsx b/2017/data-old/9_DakNong_FBOP.xls.xlsx similarity index 100% rename from data/2017-old/9_DakNong_FBOP.xls.xlsx rename to 2017/data-old/9_DakNong_FBOP.xls.xlsx diff --git a/data/2017-old/9_Khanh_Hoa_KPKQ.xls.xlsx b/2017/data-old/9_Khanh_Hoa_KPKQ.xls.xlsx similarity index 100% rename from data/2017-old/9_Khanh_Hoa_KPKQ.xls.xlsx rename to 2017/data-old/9_Khanh_Hoa_KPKQ.xls.xlsx diff --git a/data/2017-old/9_LaiChau_ALKN.xls.xlsx b/2017/data-old/9_LaiChau_ALKN.xls.xlsx similarity index 100% rename from data/2017-old/9_LaiChau_ALKN.xls.xlsx rename to 2017/data-old/9_LaiChau_ALKN.xls.xlsx diff --git a/data/2017-old/9_Vinh_Long_OLWA.xls.xlsx b/2017/data-old/9_Vinh_Long_OLWA.xls.xlsx similarity index 100% rename from data/2017-old/9_Vinh_Long_OLWA.xls.xlsx rename to 2017/data-old/9_Vinh_Long_OLWA.xls.xlsx diff --git a/data/2017-old2/1.BaRia-VungTau_PGZT.xlsx b/2017/data-old2/1.BaRia-VungTau_PGZT.xlsx similarity index 100% rename from data/2017-old2/1.BaRia-VungTau_PGZT.xlsx rename to 2017/data-old2/1.BaRia-VungTau_PGZT.xlsx diff --git a/data/2017-old2/1.Da Nang_ABWU.xlsx b/2017/data-old2/1.Da Nang_ABWU.xlsx similarity index 100% rename from data/2017-old2/1.Da Nang_ABWU.xlsx rename to 2017/data-old2/1.Da Nang_ABWU.xlsx diff --git a/data/2017-old2/1.Son La_XLFN.xlsx b/2017/data-old2/1.Son La_XLFN.xlsx similarity index 100% rename from data/2017-old2/1.Son La_XLFN.xlsx rename to 2017/data-old2/1.Son La_XLFN.xlsx diff --git a/data/2017-old2/1.TuyenQuang_PTMR.xlsx b/2017/data-old2/1.TuyenQuang_PTMR.xlsx similarity index 100% rename from data/2017-old2/1.TuyenQuang_PTMR.xlsx rename to 2017/data-old2/1.TuyenQuang_PTMR.xlsx diff --git a/data/2017-old2/10.BinhThuan_MVVG.xlsx b/2017/data-old2/10.BinhThuan_MVVG.xlsx similarity index 100% rename from data/2017-old2/10.BinhThuan_MVVG.xlsx rename to 2017/data-old2/10.BinhThuan_MVVG.xlsx diff --git a/data/2017-old2/10.LamDong_YUQA.xlsx b/2017/data-old2/10.LamDong_YUQA.xlsx similarity index 100% rename from data/2017-old2/10.LamDong_YUQA.xlsx rename to 2017/data-old2/10.LamDong_YUQA.xlsx diff --git a/data/2017-old2/10.Soc Trang_LQWU.xlsx b/2017/data-old2/10.Soc Trang_LQWU.xlsx similarity index 100% rename from data/2017-old2/10.Soc Trang_LQWU.xlsx rename to 2017/data-old2/10.Soc Trang_LQWU.xlsx diff --git a/data/2017-old2/11.BinhDuong_HVAH.xlsx b/2017/data-old2/11.BinhDuong_HVAH.xlsx similarity index 100% rename from data/2017-old2/11.BinhDuong_HVAH.xlsx rename to 2017/data-old2/11.BinhDuong_HVAH.xlsx diff --git a/data/2017-old2/11.LaoCai_ZTBP.xlsx b/2017/data-old2/11.LaoCai_ZTBP.xlsx similarity index 100% rename from data/2017-old2/11.LaoCai_ZTBP.xlsx rename to 2017/data-old2/11.LaoCai_ZTBP.xlsx diff --git a/data/2017-old2/12.BenTre_NQTU.xlsx b/2017/data-old2/12.BenTre_NQTU.xlsx similarity index 100% rename from data/2017-old2/12.BenTre_NQTU.xlsx rename to 2017/data-old2/12.BenTre_NQTU.xlsx diff --git a/data/2017-old2/12.LongAn_PDRH.xlsx b/2017/data-old2/12.LongAn_PDRH.xlsx similarity index 100% rename from data/2017-old2/12.LongAn_PDRH.xlsx rename to 2017/data-old2/12.LongAn_PDRH.xlsx diff --git a/data/2017-old2/13.NamDinh_NAYR.xlsx b/2017/data-old2/13.NamDinh_NAYR.xlsx similarity index 100% rename from data/2017-old2/13.NamDinh_NAYR.xlsx rename to 2017/data-old2/13.NamDinh_NAYR.xlsx diff --git a/data/2017-old2/13.TraVinh_FODZ.xlsx b/2017/data-old2/13.TraVinh_FODZ.xlsx similarity index 100% rename from data/2017-old2/13.TraVinh_FODZ.xlsx rename to 2017/data-old2/13.TraVinh_FODZ.xlsx diff --git a/data/2017-old2/14.NgheAn_HTKD.xlsx b/2017/data-old2/14.NgheAn_HTKD.xlsx similarity index 100% rename from data/2017-old2/14.NgheAn_HTKD.xlsx rename to 2017/data-old2/14.NgheAn_HTKD.xlsx diff --git a/data/2017-old2/15.PhuTho_IJZW.xlsx b/2017/data-old2/15.PhuTho_IJZW.xlsx similarity index 100% rename from data/2017-old2/15.PhuTho_IJZW.xlsx rename to 2017/data-old2/15.PhuTho_IJZW.xlsx diff --git a/data/2017-old2/17.QuangNam_NQMG.xlsx b/2017/data-old2/17.QuangNam_NQMG.xlsx similarity index 100% rename from data/2017-old2/17.QuangNam_NQMG.xlsx rename to 2017/data-old2/17.QuangNam_NQMG.xlsx diff --git a/data/2017-old2/18.QuangNgai_IUPY.xlsx b/2017/data-old2/18.QuangNgai_IUPY.xlsx similarity index 100% rename from data/2017-old2/18.QuangNgai_IUPY.xlsx rename to 2017/data-old2/18.QuangNgai_IUPY.xlsx diff --git a/data/2017-old2/19.QuangTri_MKNN.xlsx b/2017/data-old2/19.QuangTri_MKNN.xlsx similarity index 100% rename from data/2017-old2/19.QuangTri_MKNN.xlsx rename to 2017/data-old2/19.QuangTri_MKNN.xlsx diff --git a/data/2017-old2/2.BacKan_YQNX.xlsx b/2017/data-old2/2.BacKan_YQNX.xlsx similarity index 100% rename from data/2017-old2/2.BacKan_YQNX.xlsx rename to 2017/data-old2/2.BacKan_YQNX.xlsx diff --git a/data/2017-old2/2.Ha Giang_PIYK.xlsx b/2017/data-old2/2.Ha Giang_PIYK.xlsx similarity index 100% rename from data/2017-old2/2.Ha Giang_PIYK.xlsx rename to 2017/data-old2/2.Ha Giang_PIYK.xlsx diff --git a/data/2017-old2/2.Ninh Thuan_BAGG.xlsx b/2017/data-old2/2.Ninh Thuan_BAGG.xlsx similarity index 100% rename from data/2017-old2/2.Ninh Thuan_BAGG.xlsx rename to 2017/data-old2/2.Ninh Thuan_BAGG.xlsx diff --git a/data/2017-old2/2.Thanh Hoa_UOPE.xlsx b/2017/data-old2/2.Thanh Hoa_UOPE.xlsx similarity index 100% rename from data/2017-old2/2.Thanh Hoa_UOPE.xlsx rename to 2017/data-old2/2.Thanh Hoa_UOPE.xlsx diff --git a/data/2017-old2/2.VinhPhuc_QZJK.xlsx b/2017/data-old2/2.VinhPhuc_QZJK.xlsx similarity index 100% rename from data/2017-old2/2.VinhPhuc_QZJK.xlsx rename to 2017/data-old2/2.VinhPhuc_QZJK.xlsx diff --git a/data/2017-old2/20.TayNinh_KJAQ.xlsx b/2017/data-old2/20.TayNinh_KJAQ.xlsx similarity index 100% rename from data/2017-old2/20.TayNinh_KJAQ.xlsx rename to 2017/data-old2/20.TayNinh_KJAQ.xlsx diff --git a/data/2017-old2/21.ThaiBinh_KTQN.xlsx b/2017/data-old2/21.ThaiBinh_KTQN.xlsx similarity index 100% rename from data/2017-old2/21.ThaiBinh_KTQN.xlsx rename to 2017/data-old2/21.ThaiBinh_KTQN.xlsx diff --git a/data/2017-old2/22.ThaiNguyen_BKIF.xlsx b/2017/data-old2/22.ThaiNguyen_BKIF.xlsx similarity index 100% rename from data/2017-old2/22.ThaiNguyen_BKIF.xlsx rename to 2017/data-old2/22.ThaiNguyen_BKIF.xlsx diff --git a/data/2017-old2/24.HCM_UTLQ.xlsx b/2017/data-old2/24.HCM_UTLQ.xlsx similarity index 100% rename from data/2017-old2/24.HCM_UTLQ.xlsx rename to 2017/data-old2/24.HCM_UTLQ.xlsx diff --git a/data/2017-old2/3.BacGiang_SAVS.xlsx b/2017/data-old2/3.BacGiang_SAVS.xlsx similarity index 100% rename from data/2017-old2/3.BacGiang_SAVS.xlsx rename to 2017/data-old2/3.BacGiang_SAVS.xlsx diff --git a/data/2017-old2/3.BinhPhuoc_IPHL.xlsx b/2017/data-old2/3.BinhPhuoc_IPHL.xlsx similarity index 100% rename from data/2017-old2/3.BinhPhuoc_IPHL.xlsx rename to 2017/data-old2/3.BinhPhuoc_IPHL.xlsx diff --git a/data/2017-old2/3.Cao Bang_WMUU.xlsx b/2017/data-old2/3.Cao Bang_WMUU.xlsx similarity index 100% rename from data/2017-old2/3.Cao Bang_WMUU.xlsx rename to 2017/data-old2/3.Cao Bang_WMUU.xlsx diff --git a/data/2017-old2/3.Dong Thap_HKJX.xlsx b/2017/data-old2/3.Dong Thap_HKJX.xlsx similarity index 100% rename from data/2017-old2/3.Dong Thap_HKJX.xlsx rename to 2017/data-old2/3.Dong Thap_HKJX.xlsx diff --git a/data/2017-old2/3.Thua Thien -Hue_MAET.xlsx b/2017/data-old2/3.Thua Thien -Hue_MAET.xlsx similarity index 100% rename from data/2017-old2/3.Thua Thien -Hue_MAET.xlsx rename to 2017/data-old2/3.Thua Thien -Hue_MAET.xlsx diff --git a/data/2017-old2/4.An Giang_PMJD.xlsx b/2017/data-old2/4.An Giang_PMJD.xlsx similarity index 100% rename from data/2017-old2/4.An Giang_PMJD.xlsx rename to 2017/data-old2/4.An Giang_PMJD.xlsx diff --git a/data/2017-old2/4.BacNinh_NNIS.xlsx b/2017/data-old2/4.BacNinh_NNIS.xlsx similarity index 100% rename from data/2017-old2/4.BacNinh_NNIS.xlsx rename to 2017/data-old2/4.BacNinh_NNIS.xlsx diff --git a/data/2017-old2/4.Binh Dinh_VOMJ.xlsx b/2017/data-old2/4.Binh Dinh_VOMJ.xlsx similarity index 100% rename from data/2017-old2/4.Binh Dinh_VOMJ.xlsx rename to 2017/data-old2/4.Binh Dinh_VOMJ.xlsx diff --git a/data/2017-old2/4.DienBien_FYGN.xlsx b/2017/data-old2/4.DienBien_FYGN.xlsx similarity index 100% rename from data/2017-old2/4.DienBien_FYGN.xlsx rename to 2017/data-old2/4.DienBien_FYGN.xlsx diff --git a/data/2017-old2/4.Lang Son_QWOG.xlsx b/2017/data-old2/4.Lang Son_QWOG.xlsx similarity index 100% rename from data/2017-old2/4.Lang Son_QWOG.xlsx rename to 2017/data-old2/4.Lang Son_QWOG.xlsx diff --git a/data/2017-old2/5.Bac Lieu_VIVY.xlsx b/2017/data-old2/5.Bac Lieu_VIVY.xlsx similarity index 100% rename from data/2017-old2/5.Bac Lieu_VIVY.xlsx rename to 2017/data-old2/5.Bac Lieu_VIVY.xlsx diff --git a/data/2017-old2/5.Gia Lai_TAAS.xlsx b/2017/data-old2/5.Gia Lai_TAAS.xlsx similarity index 100% rename from data/2017-old2/5.Gia Lai_TAAS.xlsx rename to 2017/data-old2/5.Gia Lai_TAAS.xlsx diff --git a/data/2017-old2/5.HaiDuong_WNHD.xlsx b/2017/data-old2/5.HaiDuong_WNHD.xlsx similarity index 100% rename from data/2017-old2/5.HaiDuong_WNHD.xlsx rename to 2017/data-old2/5.HaiDuong_WNHD.xlsx diff --git a/data/2017-old2/5.Hanam_SDKN.xlsx b/2017/data-old2/5.Hanam_SDKN.xlsx similarity index 100% rename from data/2017-old2/5.Hanam_SDKN.xlsx rename to 2017/data-old2/5.Hanam_SDKN.xlsx diff --git a/data/2017-old2/5.Yen Bai_BSLV.xlsx b/2017/data-old2/5.Yen Bai_BSLV.xlsx similarity index 100% rename from data/2017-old2/5.Yen Bai_BSLV.xlsx rename to 2017/data-old2/5.Yen Bai_BSLV.xlsx diff --git a/data/2017-old2/6.Hau Giang_SIAJ.xlsx b/2017/data-old2/6.Hau Giang_SIAJ.xlsx similarity index 100% rename from data/2017-old2/6.Hau Giang_SIAJ.xlsx rename to 2017/data-old2/6.Hau Giang_SIAJ.xlsx diff --git a/data/2017-old2/6.HoaBinh_HLYQ.xlsx b/2017/data-old2/6.HoaBinh_HLYQ.xlsx similarity index 100% rename from data/2017-old2/6.HoaBinh_HLYQ.xlsx rename to 2017/data-old2/6.HoaBinh_HLYQ.xlsx diff --git a/data/2017-old2/6.NinhBinh_PKMQ.xlsx b/2017/data-old2/6.NinhBinh_PKMQ.xlsx similarity index 100% rename from data/2017-old2/6.NinhBinh_PKMQ.xlsx rename to 2017/data-old2/6.NinhBinh_PKMQ.xlsx diff --git a/data/2017-old2/6.Quang Ninh_YAKJ.xlsx b/2017/data-old2/6.Quang Ninh_YAKJ.xlsx similarity index 100% rename from data/2017-old2/6.Quang Ninh_YAKJ.xlsx rename to 2017/data-old2/6.Quang Ninh_YAKJ.xlsx diff --git a/data/2017-old2/7.HaTinh_XMFQ.xlsx b/2017/data-old2/7.HaTinh_XMFQ.xlsx similarity index 100% rename from data/2017-old2/7.HaTinh_XMFQ.xlsx rename to 2017/data-old2/7.HaTinh_XMFQ.xlsx diff --git a/data/2017-old2/7.HungYen_TCBE.xlsx b/2017/data-old2/7.HungYen_TCBE.xlsx similarity index 100% rename from data/2017-old2/7.HungYen_TCBE.xlsx rename to 2017/data-old2/7.HungYen_TCBE.xlsx diff --git a/data/2017-old2/7.Kon Tum_CAQU.xlsx b/2017/data-old2/7.Kon Tum_CAQU.xlsx similarity index 100% rename from data/2017-old2/7.Kon Tum_CAQU.xlsx rename to 2017/data-old2/7.Kon Tum_CAQU.xlsx diff --git a/data/2017-old2/7.Tien Giang_AOWZ.xlsx b/2017/data-old2/7.Tien Giang_AOWZ.xlsx similarity index 100% rename from data/2017-old2/7.Tien Giang_AOWZ.xlsx rename to 2017/data-old2/7.Tien Giang_AOWZ.xlsx diff --git a/data/2017-old2/8.PhuYen_MLTQ.xlsx b/2017/data-old2/8.PhuYen_MLTQ.xlsx similarity index 100% rename from data/2017-old2/8.PhuYen_MLTQ.xlsx rename to 2017/data-old2/8.PhuYen_MLTQ.xlsx diff --git a/data/2017-old2/9.DakNong_ZWJT.xlsx b/2017/data-old2/9.DakNong_ZWJT.xlsx similarity index 100% rename from data/2017-old2/9.DakNong_ZWJT.xlsx rename to 2017/data-old2/9.DakNong_ZWJT.xlsx diff --git a/data/2017-old2/9.Khanh Hoa_IBTX.xlsx b/2017/data-old2/9.Khanh Hoa_IBTX.xlsx similarity index 100% rename from data/2017-old2/9.Khanh Hoa_IBTX.xlsx rename to 2017/data-old2/9.Khanh Hoa_IBTX.xlsx diff --git a/data/2017-old2/9.Vinh Long_WVKI.xlsx b/2017/data-old2/9.Vinh Long_WVKI.xlsx similarity index 100% rename from data/2017-old2/9.Vinh Long_WVKI.xlsx rename to 2017/data-old2/9.Vinh Long_WVKI.xlsx diff --git a/data/2017/an-giang.xls b/2017/data/an-giang.xls similarity index 100% rename from data/2017/an-giang.xls rename to 2017/data/an-giang.xls diff --git a/data/2017/ba-ria-vung-tau.xls b/2017/data/ba-ria-vung-tau.xls similarity index 100% rename from data/2017/ba-ria-vung-tau.xls rename to 2017/data/ba-ria-vung-tau.xls diff --git a/data/2017/bac-giang.xls b/2017/data/bac-giang.xls similarity index 100% rename from data/2017/bac-giang.xls rename to 2017/data/bac-giang.xls diff --git a/data/2017/bac-kan.xls b/2017/data/bac-kan.xls similarity index 100% rename from data/2017/bac-kan.xls rename to 2017/data/bac-kan.xls diff --git a/data/2017/bac-lieu.xls b/2017/data/bac-lieu.xls similarity index 100% rename from data/2017/bac-lieu.xls rename to 2017/data/bac-lieu.xls diff --git a/data/2017/bac-ninh.xls b/2017/data/bac-ninh.xls similarity index 100% rename from data/2017/bac-ninh.xls rename to 2017/data/bac-ninh.xls diff --git a/data/2017/ben-tre.xls b/2017/data/ben-tre.xls similarity index 100% rename from data/2017/ben-tre.xls rename to 2017/data/ben-tre.xls diff --git a/data/2017/binh-dinh.xls b/2017/data/binh-dinh.xls similarity index 100% rename from data/2017/binh-dinh.xls rename to 2017/data/binh-dinh.xls diff --git a/data/2017/binh-duong.xls b/2017/data/binh-duong.xls similarity index 100% rename from data/2017/binh-duong.xls rename to 2017/data/binh-duong.xls diff --git a/data/2017/binh-phuoc.xls b/2017/data/binh-phuoc.xls similarity index 100% rename from data/2017/binh-phuoc.xls rename to 2017/data/binh-phuoc.xls diff --git a/data/2017/binh-thuan.xls b/2017/data/binh-thuan.xls similarity index 100% rename from data/2017/binh-thuan.xls rename to 2017/data/binh-thuan.xls diff --git a/data/2017/ca-mau.xls b/2017/data/ca-mau.xls similarity index 100% rename from data/2017/ca-mau.xls rename to 2017/data/ca-mau.xls diff --git a/data/2017/can-tho.xls b/2017/data/can-tho.xls similarity index 100% rename from data/2017/can-tho.xls rename to 2017/data/can-tho.xls diff --git a/data/2017/cao-bang.xls b/2017/data/cao-bang.xls similarity index 100% rename from data/2017/cao-bang.xls rename to 2017/data/cao-bang.xls diff --git a/data/2017/da-nang.xls b/2017/data/da-nang.xls similarity index 100% rename from data/2017/da-nang.xls rename to 2017/data/da-nang.xls diff --git a/data/2017/dak-lak.xls b/2017/data/dak-lak.xls similarity index 100% rename from data/2017/dak-lak.xls rename to 2017/data/dak-lak.xls diff --git a/data/2017/dak-nong.xls b/2017/data/dak-nong.xls similarity index 100% rename from data/2017/dak-nong.xls rename to 2017/data/dak-nong.xls diff --git a/data/2017/dien-bien.xls b/2017/data/dien-bien.xls similarity index 100% rename from data/2017/dien-bien.xls rename to 2017/data/dien-bien.xls diff --git a/data/2017/dong-nai.xls b/2017/data/dong-nai.xls similarity index 100% rename from data/2017/dong-nai.xls rename to 2017/data/dong-nai.xls diff --git a/data/2017/dong-thap.xls b/2017/data/dong-thap.xls similarity index 100% rename from data/2017/dong-thap.xls rename to 2017/data/dong-thap.xls diff --git a/data/2017/gia-lai.xls b/2017/data/gia-lai.xls similarity index 100% rename from data/2017/gia-lai.xls rename to 2017/data/gia-lai.xls diff --git a/data/2017/ha-giang.xls b/2017/data/ha-giang.xls similarity index 100% rename from data/2017/ha-giang.xls rename to 2017/data/ha-giang.xls diff --git a/data/2017/ha-nam.xls b/2017/data/ha-nam.xls similarity index 100% rename from data/2017/ha-nam.xls rename to 2017/data/ha-nam.xls diff --git a/data/2017/ha-noi.xls b/2017/data/ha-noi.xls similarity index 100% rename from data/2017/ha-noi.xls rename to 2017/data/ha-noi.xls diff --git a/data/2017/ha-tinh.xls b/2017/data/ha-tinh.xls similarity index 100% rename from data/2017/ha-tinh.xls rename to 2017/data/ha-tinh.xls diff --git a/data/2017/hai-duong.xls b/2017/data/hai-duong.xls similarity index 100% rename from data/2017/hai-duong.xls rename to 2017/data/hai-duong.xls diff --git a/data/2017/hai-phong.xls b/2017/data/hai-phong.xls similarity index 100% rename from data/2017/hai-phong.xls rename to 2017/data/hai-phong.xls diff --git a/data/2017/hau-giang.xls b/2017/data/hau-giang.xls similarity index 100% rename from data/2017/hau-giang.xls rename to 2017/data/hau-giang.xls diff --git a/data/2017/ho-chi-minh.xls b/2017/data/ho-chi-minh.xls similarity index 100% rename from data/2017/ho-chi-minh.xls rename to 2017/data/ho-chi-minh.xls diff --git a/data/2017/hoa-binh.xls b/2017/data/hoa-binh.xls similarity index 100% rename from data/2017/hoa-binh.xls rename to 2017/data/hoa-binh.xls diff --git a/data/2017/hung-yen.xls b/2017/data/hung-yen.xls similarity index 100% rename from data/2017/hung-yen.xls rename to 2017/data/hung-yen.xls diff --git a/data/2017/khanh-hoa.xls b/2017/data/khanh-hoa.xls similarity index 100% rename from data/2017/khanh-hoa.xls rename to 2017/data/khanh-hoa.xls diff --git a/data/2017/kien-giang.xls b/2017/data/kien-giang.xls similarity index 100% rename from data/2017/kien-giang.xls rename to 2017/data/kien-giang.xls diff --git a/data/2017/kon-tum.xls b/2017/data/kon-tum.xls similarity index 100% rename from data/2017/kon-tum.xls rename to 2017/data/kon-tum.xls diff --git a/data/2017/lai-chau.xls b/2017/data/lai-chau.xls similarity index 100% rename from data/2017/lai-chau.xls rename to 2017/data/lai-chau.xls diff --git a/data/2017/lam-dong.xls b/2017/data/lam-dong.xls similarity index 100% rename from data/2017/lam-dong.xls rename to 2017/data/lam-dong.xls diff --git a/data/2017/lang-son.xls b/2017/data/lang-son.xls similarity index 100% rename from data/2017/lang-son.xls rename to 2017/data/lang-son.xls diff --git a/data/2017/lao-cai.xls b/2017/data/lao-cai.xls similarity index 100% rename from data/2017/lao-cai.xls rename to 2017/data/lao-cai.xls diff --git a/data/2017/long-an.xls b/2017/data/long-an.xls similarity index 100% rename from data/2017/long-an.xls rename to 2017/data/long-an.xls diff --git a/data/2017/nam-dinh.xls b/2017/data/nam-dinh.xls similarity index 100% rename from data/2017/nam-dinh.xls rename to 2017/data/nam-dinh.xls diff --git a/data/2017/nghe-an.xls b/2017/data/nghe-an.xls similarity index 100% rename from data/2017/nghe-an.xls rename to 2017/data/nghe-an.xls diff --git a/data/2017/ninh-binh.xls b/2017/data/ninh-binh.xls similarity index 100% rename from data/2017/ninh-binh.xls rename to 2017/data/ninh-binh.xls diff --git a/data/2017/ninh-thuan.xls b/2017/data/ninh-thuan.xls similarity index 100% rename from data/2017/ninh-thuan.xls rename to 2017/data/ninh-thuan.xls diff --git a/data/2017/phu-tho.xls b/2017/data/phu-tho.xls similarity index 100% rename from data/2017/phu-tho.xls rename to 2017/data/phu-tho.xls diff --git a/data/2017/phu-yen.xls b/2017/data/phu-yen.xls similarity index 100% rename from data/2017/phu-yen.xls rename to 2017/data/phu-yen.xls diff --git a/data/2017/quang-binh.xls b/2017/data/quang-binh.xls similarity index 100% rename from data/2017/quang-binh.xls rename to 2017/data/quang-binh.xls diff --git a/data/2017/quang-nam.xls b/2017/data/quang-nam.xls similarity index 100% rename from data/2017/quang-nam.xls rename to 2017/data/quang-nam.xls diff --git a/data/2017/quang-ngai.xls b/2017/data/quang-ngai.xls similarity index 100% rename from data/2017/quang-ngai.xls rename to 2017/data/quang-ngai.xls diff --git a/data/2017/quang-ninh.xls b/2017/data/quang-ninh.xls similarity index 100% rename from data/2017/quang-ninh.xls rename to 2017/data/quang-ninh.xls diff --git a/data/2017/quang-tri.xls b/2017/data/quang-tri.xls similarity index 100% rename from data/2017/quang-tri.xls rename to 2017/data/quang-tri.xls diff --git a/data/2017/soc-trang.xls b/2017/data/soc-trang.xls similarity index 100% rename from data/2017/soc-trang.xls rename to 2017/data/soc-trang.xls diff --git a/data/2017/son-la.xls b/2017/data/son-la.xls similarity index 100% rename from data/2017/son-la.xls rename to 2017/data/son-la.xls diff --git a/data/2017/tay-ninh.xls b/2017/data/tay-ninh.xls similarity index 100% rename from data/2017/tay-ninh.xls rename to 2017/data/tay-ninh.xls diff --git a/data/2017/thai-binh.xls b/2017/data/thai-binh.xls similarity index 100% rename from data/2017/thai-binh.xls rename to 2017/data/thai-binh.xls diff --git a/data/2017/thai-nguyen.xls b/2017/data/thai-nguyen.xls similarity index 100% rename from data/2017/thai-nguyen.xls rename to 2017/data/thai-nguyen.xls diff --git a/data/2017/thanh-hoa.xls b/2017/data/thanh-hoa.xls similarity index 100% rename from data/2017/thanh-hoa.xls rename to 2017/data/thanh-hoa.xls diff --git a/data/2017/thua-thien-hue.xls b/2017/data/thua-thien-hue.xls similarity index 100% rename from data/2017/thua-thien-hue.xls rename to 2017/data/thua-thien-hue.xls diff --git a/data/2017/tien-giang.xls b/2017/data/tien-giang.xls similarity index 100% rename from data/2017/tien-giang.xls rename to 2017/data/tien-giang.xls diff --git a/data/2017/tra-vinh.xls b/2017/data/tra-vinh.xls similarity index 100% rename from data/2017/tra-vinh.xls rename to 2017/data/tra-vinh.xls diff --git a/data/2017/tuyen-quang.xls b/2017/data/tuyen-quang.xls similarity index 100% rename from data/2017/tuyen-quang.xls rename to 2017/data/tuyen-quang.xls diff --git a/data/2017/vinh-long.xls b/2017/data/vinh-long.xls similarity index 100% rename from data/2017/vinh-long.xls rename to 2017/data/vinh-long.xls diff --git a/data/2017/vinh-phuc.xls b/2017/data/vinh-phuc.xls similarity index 100% rename from data/2017/vinh-phuc.xls rename to 2017/data/vinh-phuc.xls diff --git a/data/2017/yen-bai.xls b/2017/data/yen-bai.xls similarity index 100% rename from data/2017/yen-bai.xls rename to 2017/data/yen-bai.xls diff --git a/2017/docs/README.md b/2017/docs/README.md new file mode 100644 index 0000000..a16c8b6 --- /dev/null +++ b/2017/docs/README.md @@ -0,0 +1,5 @@ +# Docs + +- [`system-architecture.md`](./system-architecture.md) — data flow, schema, 3-variant deploy, score-tier model, admission-block catalog +- [`data-pipeline.md`](./data-pipeline.md) — Excel parse quirks, overflow-sheet gotcha, audit scripts, refresh flow +- [`deployment-guide.md`](./deployment-guide.md) — GitHub Pages workflow, adding a new variant, rollback diff --git a/2017/docs/data-pipeline.md b/2017/docs/data-pipeline.md new file mode 100644 index 0000000..589f7d4 --- /dev/null +++ b/2017/docs/data-pipeline.md @@ -0,0 +1,110 @@ +# Data Pipeline + +From raw Excel files to a compressed SQLite file the browser can load. + +## Canonical source (data/) + +63 Excel files from baotintuc.vn CDN. Source article: +`https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm` + +Re-download anytime: + +```bash +node scripts/crawl-baotintuc.js +``` + +Idempotent — skips files already present. Saves to `data/.xls`. + +## Reference sources (data-old/, data-old2/) + +Both were collected from Vietnamese news sites around the time of the 2017 exam. +Specific publisher URLs were not recorded and cannot be recovered, so these +datasets are **not reproducible** — the files committed in git are the only +copy. They are kept for historical comparison and to expose the deploy matrix; +the main deployment always uses the reproducible `data/` source. + +## Source Excel shape + +Every file is a single sheet (for `data-old/`) or up to 3 sheets (for `data/` .xls) with columns: + +| Col | Name | Content | +|---|---|---| +| 0 | HO_TEN | full name in Vietnamese | +| 1 | NGAY_SINH | `dd/mm/yyyy` | +| 2 | SOBAODANH | 8-digit string, first 2 digits = province | +| 3 | DIEM_THI | concatenated per-subject score string in the source language, e.g. `"Toan: 6.80 Van: 5.25 ... English: 5.80"` | + +Some files lack a header row. `isHeaderRow()` in `build-lib.js` detects and skips. + +## Score text parsing + +`SCORE_PATTERNS` in `build-lib.js` defines one regex per subject. The patterns must match the exact subject labels the source files use (Vietnamese). + +Subjects supported (DB column / label in source text): + +| Column | Source label | English | +|---|---|---| +| toan | Toán | Math | +| ngu_van | Ngữ văn | Literature | +| vat_ly | Vật lí | Physics | +| hoa_hoc | Hóa học | Chemistry | +| sinh_hoc | Sinh học | Biology | +| khtn | KHTN | Natural Sciences combined | +| lich_su | Lịch sử | History | +| dia_ly | Địa lí | Geography | +| gdcd | GDCD | Civic Education | +| khxh | KHXH | Social Sciences combined | +| tieng_anh | Tiếng Anh | English | +| tieng_phap | Tiếng Pháp | French | +| tieng_nga | Tiếng Nga | Russian | +| tieng_trung | Tiếng Trung | Chinese | + +Missing-in-string → `NULL` in DB (student didn't take that subject). + +## Why three parsers + +One shared lib + three thin parsers, not one parser with flags. Each dataset's quirks stay visible in its own file: + +| File | Source dir | Sheet strategy | Extra guard | +|---|---|---|---| +| `build-database.js` | `data/` | iterate ALL sheets (.xls row overflow) | — | +| `build-database-old.js` | `data-old/` | sheet 0 only | reject rows where `so_bao_danh` isn't digits-only (rejects an `HO_TEN` → `SOBAODANH` header leak present in this export) | +| `build-database-old2.js` | `data-old2/` | iterate ALL sheets (HCM overflow) | skip fully blank rows before counting | + +## Overflow-sheet gotcha + +The old `.xls` binary format caps at 65,536 rows per sheet. Hanoi and Ho Chi Minh City exceed that in the baotintuc export, so Excel auto-splits the overflow into `Sheet2`. **Only reading `Sheet1` silently drops 13,720 students** (Hanoi +7,275, HCM +6,445). + +The audit `node scripts/audit-row-counts.js` catches this by comparing source-row-count (across all sheets) to DB-row-count. + +## Audits + +Run any time after a rebuild: + +```bash +node scripts/audit-row-counts.js # source rows vs DB rows (zero-loss check) +node scripts/check-duplicates.js # md5 + row-content duplicates in data/ +node scripts/diff-datasets.js # compare two DBs (needs backup/ dir) +``` + +Expected baseline: + +| DB | Source rows | Skipped | DB rows | +|---|---|---|---| +| main | 861,131 | 63 empty rows | **861,068** | +| old | 847,349 | 1 header-leak | **847,348** | +| old2 | 679,764 | 0 | **679,764** | + +## Refreshing the data + +```bash +rm data/*.xls # clear current snapshot +node scripts/crawl-baotintuc.js # re-download from CDN +npm run build:db # rebuild +node scripts/audit-row-counts.js # verify +gzip -kf -9 public/thptqg2017.db # compress for shipping +``` + +## Adding a new source dataset + +See `docs/deployment-guide.md` → "Adding a new variant". diff --git a/2017/docs/deployment-guide.md b/2017/docs/deployment-guide.md new file mode 100644 index 0000000..4410294 --- /dev/null +++ b/2017/docs/deployment-guide.md @@ -0,0 +1,52 @@ +# Deployment Guide + +Deploys to GitHub Pages via `.github/workflows/deploy.yml`. Every push to `main` rebuilds and redeploys all 3 variants. + +## What the workflow does + +1. Checkout, setup Node 20, `npm ci` +2. `npm run build:db:all` — builds 3 SQLite DBs (main, old, old2) +3. Gzip each DB at level 9 +4. `npm run build:all` — builds 3 Vite bundles into `dist/`, `dist/old/`, `dist/old2/` +5. Remove uncompressed `.db` files from `dist/` (only `.gz` ships) +6. `actions/upload-pages-artifact` + `actions/deploy-pages` + +Total CI time ≈ 4–6 min (DB build dominates). + +## Resulting URLs + +- `https://.github.io/thptqg2017/` +- `https://.github.io/thptqg2017/old/` +- `https://.github.io/thptqg2017/old2/` + +## Local reproduction + +```bash +npm ci +npm run build:db:all +gzip -kf -9 public/thptqg2017.db public-old/thptqg2017.db public-old2/thptqg2017.db +npm run build:all +npx serve dist # or any static server +``` + +## Adding a new variant + +1. Drop source Excel files into a new `data-vN/` folder +2. Add `public-vN/` to `.gitignore` patterns for the `.db` + `.db.gz` +3. Copy `scripts/build-database-old.js` → `scripts/build-database-vN.js`; update `SRC_DIR` / `DB_PATH`; adjust parse logic for the new quirks (multi-sheet? blank rows? numeric guard?) +4. Add variant to `VARIANT_CONFIG` in `vite.config.js`: + ```js + vN: { base: "/thptqg2017/vN/", publicDir: "public-vN", outDir: "dist/vN" } + ``` +5. Add `build:db:vN` and `build:vN` npm scripts; wire them into `build:db:all` and `build:all` +6. Update `.github/workflows/deploy.yml` — add gzip step + rm step for the new DB + +## Notes + +- **DB gzip is non-cacheable across deploys** — every rebuild produces a new `.db.gz` (not byte-identical due to SQLite page shuffling). First-visit users pay the 47 MB download; subsequent visits hit browser cache until the next deploy. +- **GitHub Pages file size limit**: individual files ≤ 100 MB. Main gzipped DB is ~47 MB — safe. Uncompressed 159 MB DB would not fit; that's why we ship `.gz` and the frontend decompresses via `DecompressionStream`. +- **No server-side compression assumption.** The app reads the `.gz` file directly (not `Content-Encoding: gzip`). GH Pages does not reliably gzip on-the-fly for arbitrary paths; shipping pre-gzipped bytes is deterministic. + +## Rollback + +Deploys are stateless snapshots. To rollback, revert the commit on `main` and push — the next workflow rebuilds the older state. There's no state to migrate. diff --git a/2017/docs/system-architecture.md b/2017/docs/system-architecture.md new file mode 100644 index 0000000..aa290cb --- /dev/null +++ b/2017/docs/system-architecture.md @@ -0,0 +1,97 @@ +# System Architecture + +Static site. No backend. Browser downloads a compressed SQLite file at boot, then every query runs locally via `sql.js` (WASM). + +## Data flow + +``` +Excel files (.xls / .xlsx) + │ + ▼ build-database*.js (Node) +SQLite DB (public*/thptqg2017.db) + │ + ▼ gzip -9 +thptqg2017.db.gz (~47 MB for main variant) + │ + ▼ Vite build copies public*/ into dist/ +Static site on GitHub Pages + │ + ▼ browser loads +sql.js (WASM) opens the .db → queries run client-side +``` + +## Three deployment variants + +One repo → three independent sites, same frontend, different dataset. + +| Variant | Route | Source dir | Public dir | Build cmd | +|---|---|---|---|---| +| main | `/thptqg2017/` | `data/` | `public/` | `npm run build` | +| old | `/thptqg2017/old/` | `data-old/` | `public-old/` | `npm run build:old` | +| old2 | `/thptqg2017/old2/` | `data-old2/` | `public-old2/` | `npm run build:old2` | + +`vite.config.js` reads `process.env.VARIANT` and switches `base`, `publicDir`, `outDir` accordingly. `emptyOutDir` is on only for the main build so the variant builds merge into `dist/old/`, `dist/old2/` cleanly. + +## Schema (all 3 DBs share this) + +```sql +CREATE TABLE student ( + so_bao_danh TEXT PRIMARY KEY, -- exam ID (8 digits, first 2 = province) + ho_ten TEXT NOT NULL, + ho_ten_ascii TEXT NOT NULL, -- NFD-stripped lowercase for accent-insensitive search + ngay_sinh TEXT, -- dd/mm/yyyy + toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, + lich_su, dia_ly, gdcd, khxh, + tieng_anh, tieng_phap, tieng_nga, tieng_trung REAL +); +CREATE INDEX idx_ho_ten ON student(ho_ten); +CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); +``` + +No German / Japanese language columns — neither appears in any source file. + +Each score column is `NULL` when the student didn't take that subject. A student's "admission block" total is only computed when all 3 required subjects are non-null. + +## Parse quirks (why 3 builder scripts) + +Each dataset had a different quirk. Rather than one mega-parser with mode flags, each builder script is ~70 lines and isolates its own workarounds. + +| Dataset | Quirk | Mitigation | +|---|---|---| +| data/ (baotintuc .xls) | Hanoi + HCM overflow past the 65,536 row-per-sheet .xls limit | Iterate all `wb.SheetNames` | +| data-old/ (xlsx) | Single-sheet always; one bogus header row (`SOBAODANH`/`HO_TEN`) leaked into an earlier DB | Strict numeric-SBD guard rejects the leak | +| data-old2/ (xlsx) | HCM overflow + many blank trailing rows | Multi-sheet walk + blank-row pre-filter | + +Shared concerns (regex, schema, `toAscii`, header detection) live in `scripts/build-lib.js`. + +## Admission blocks + +Vietnamese universities admit students based on 3-subject combinations called "admission blocks" (khối thi). `src/lib/admission-blocks.js` lists all 49 official 2017 blocks computable from our schema (A00–A11, B00–B08, C00–C20, D01–D15). Each entry is `{ code, subjects: [3 keys], label }`. + +`computeBlocks(student)` returns only blocks where the student has all 3 subject scores, sorted by total desc. Used by the student detail card. + +The SQL preset "Top 10 best-block in Long An" materialises all 49 blocks as a `UNION ALL` CTE, picks each student's best block via `ROW_NUMBER() OVER (PARTITION BY so_bao_danh ORDER BY s DESC, k)`, then ranks across students. + +## Score tiers (UI coding) + +6-level TFT rarity ladder (white → green → blue → purple → gold → prismatic). Defined in `scoreTier()` in `src/lib/admission-blocks.js`. + +| Tier | Range | UI label (Vietnamese) | English | +|---|---|---|---| +| common | ≤ 1 | Điểm liệt | Disqualifying | +| uncommon | < 5 | Chưa đạt | Below passing | +| rare | 5–6.5 | Trung bình | Average | +| epic | 6.5–8 | Khá | Good | +| legendary | 8–9 | Giỏi | Very good | +| prismatic | 9–10 | Xuất sắc | Excellent | + +Color + icon + text label — never color-only. + +## Frontend key behaviors + +- Query state lives in `App.jsx` and is synced to URL `?q=...` via `history.replaceState` — bookmarkable, shareable. +- `SearchForm` is a controlled component that 300ms-debounces input changes and auto-triggers search once the query is long enough (3+ digits for SBD, 2+ chars for name). +- Detection: all-digits → exact `so_bao_danh` match; ASCII-only → folded `ho_ten_ascii LIKE`; has Vietnamese diacritics → search both `ho_ten` and folded column. +- Exactly 1 result → `StudentDetail` card; 0 or >1 → `ScoreTable`. +- Global `/` key focuses the search input when not already typing. +- Share button on the detail card uses Web Share API when present, else clipboard with a pre-formatted summary + deep-link URL. diff --git a/eslint.config.js b/2017/eslint.config.js similarity index 79% rename from eslint.config.js rename to 2017/eslint.config.js index 78101ed..302e607 100644 --- a/eslint.config.js +++ b/2017/eslint.config.js @@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist', '.build', '_site']), + globalIgnores(['dist']), { files: ['**/*.{js,jsx}'], extends: [ @@ -27,8 +27,8 @@ export default defineConfig([ }, }, { - // Node-executed files (Vite config, parser tooling) run with Node globals. - files: ['vite.config.js', 'scripts/**/*.js', 'parser/scripts/**/*.js'], + // Node-executed files (Vite config, build scripts) run with Node globals. + files: ['vite.config.js', 'scripts/**/*.js'], languageOptions: { globals: { ...globals.node }, }, diff --git a/2017/index.html b/2017/index.html new file mode 100644 index 0000000..6961ca3 --- /dev/null +++ b/2017/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + Tra cứu điểm thi THPT QG 2017 + + +
+ + + diff --git a/2017/package.json b/2017/package.json new file mode 100644 index 0000000..280936d --- /dev/null +++ b/2017/package.json @@ -0,0 +1,43 @@ +{ + "name": "thptqg2017", + "private": true, + "packageManager": "pnpm@11.1.1", + "version": "1.0.0", + "type": "module", + "description": "Tra cứu điểm thi THPT QG 2017", + "scripts": { + "build:rust": "cargo build --release --manifest-path tools/xlsxread/Cargo.toml", + "build:db": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data.toml --input data --output public/thptqg2017.db", + "build:db:old": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old.toml --input data-old --output public-old/thptqg2017.db", + "build:db:old2": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old2.toml --input data-old2 --output public-old2/thptqg2017.db", + "build:db:all": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data.toml --input data --output public/thptqg2017.db && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old.toml --input data-old --output public-old/thptqg2017.db && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old2.toml --input data-old2 --output public-old2/thptqg2017.db", + "dev": "vite", + "build": "vite build", + "build:old": "node -e \"process.env.VARIANT='old';require('child_process').spawnSync('npx',['vite','build'],{stdio:'inherit',shell:true,env:process.env})\"", + "build:old2": "node -e \"process.env.VARIANT='old2';require('child_process').spawnSync('npx',['vite','build'],{stdio:'inherit',shell:true,env:process.env})\"", + "build:all": "pnpm build && pnpm build:old && pnpm build:old2", + "preview": "vite preview", + "lint": "eslint ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tiennm99/thptqg2017.git" + }, + "license": "ISC", + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "sql.js": "^1.14.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.16" + } +} diff --git a/2017/pnpm-lock.yaml b/2017/pnpm-lock.yaml new file mode 100644 index 0000000..ce02b86 --- /dev/null +++ b/2017/pnpm-lock.yaml @@ -0,0 +1,1624 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^19.2.4 + version: 19.2.6 + react-dom: + specifier: ^19.2.4 + version: 19.2.6(react@19.2.6) + sql.js: + specifier: ^1.14.1 + version: 1.14.1 + devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.16) + eslint: + specifier: ^9.39.4 + version: 9.39.4 + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.1.1(eslint@9.39.4) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.4) + globals: + specifier: ^17.4.0 + version: 17.6.0 + vite: + specifier: ^8.0.16 + version: 8.0.16 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.10.29: + resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.353: + resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sql.js@1.14.1: + resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.133.0': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@rolldown/pluginutils@1.0.1': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.0.1(vite@8.0.16)': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.16 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.10.29: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.29 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.353 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001792: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.353: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + eslint: 9.39.4 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@17.6.0: {} + + has-flag@4.0.0: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.38: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + resolve-from@4.0.0: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + sql.js@1.14.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.16: + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/2017/pnpm-workspace.yaml b/2017/pnpm-workspace.yaml new file mode 100644 index 0000000..65783dc --- /dev/null +++ b/2017/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + better-sqlite3: true diff --git a/parser/scripts/check-duplicates.js b/2017/scripts/check-duplicates.js similarity index 74% rename from parser/scripts/check-duplicates.js rename to 2017/scripts/check-duplicates.js index c0b4a53..7b17e1f 100644 --- a/parser/scripts/check-duplicates.js +++ b/2017/scripts/check-duplicates.js @@ -1,8 +1,4 @@ -// One-off audit: detect content-identical Excel files via md5. -// -// BROKEN as committed — `dirs` below is a hardcoded Windows path from the -// original author's machine. Predates the repo unification; left unchanged -// rather than half-fixed. Point `dirs` at data/ to use it. +// One-off audit: detect content-identical Excel files via md5 import crypto from "crypto"; import fs from "fs"; import path from "path"; diff --git a/parser/scripts/crawl-baotintuc.js b/2017/scripts/crawl-baotintuc.js similarity index 97% rename from parser/scripts/crawl-baotintuc.js rename to 2017/scripts/crawl-baotintuc.js index 8f4c20c..7fb8036 100644 --- a/parser/scripts/crawl-baotintuc.js +++ b/2017/scripts/crawl-baotintuc.js @@ -1,12 +1,12 @@ // Crawl Excel score files for all 63 provinces from baotintuc.vn article: // https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm -// Saves to data/2017/.xls (63 files). Idempotent: skips already-downloaded files. +// Saves to data/.xls (63 files). Idempotent: skips already-downloaded files. import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const OUT_DIR = path.join(__dirname, "..", "..", "data", "2017"); +const OUT_DIR = path.join(__dirname, "..", "data"); const LINKS = [ ["An Giang", "https://cdnmedia.baotintuc.vn/2017/07/06/17/57/Angiang.xls"], diff --git a/parser/scripts/diff-datasets.js b/2017/scripts/diff-datasets.js similarity index 90% rename from parser/scripts/diff-datasets.js rename to 2017/scripts/diff-datasets.js index affcae8..6df1d20 100644 --- a/parser/scripts/diff-datasets.js +++ b/2017/scripts/diff-datasets.js @@ -1,11 +1,4 @@ -// Compare two builds of the 2017 database, column by column. -// -// BROKEN as committed, for two reasons that both predate the repo unification: -// - `better-sqlite3` is imported but declared in no package.json -// - the public/ and backup/ paths it reads no longer exist -// -// Left unchanged rather than half-fixed. For a working comparison of two -// builds use db-stats.js + verify-parity.js, which need no dependencies. +// Compare new DB (public/thptqg2017.db) vs backup old DB (backup/thptqg2017.old.db) import Database from "better-sqlite3"; const NEW = new Database("public/thptqg2017.db", { readonly: true }); diff --git a/src/App.css b/2017/src/App.css similarity index 90% rename from src/App.css rename to 2017/src/App.css index 376bd76..af95038 100644 --- a/src/App.css +++ b/2017/src/App.css @@ -779,101 +779,3 @@ footer { transition-duration: 0.01ms !important; } } - -/* --------------------------------------------------------------------------- - Table cells for the 2016-only identity columns. - Ported from the 2016 frontend; hidden automatically on the 2017 datasets - because every row there is NULL. - --------------------------------------------------------------------------- */ - -.sbd-cell { - font-family: ui-monospace, monospace; - font-size: 0.85rem; - color: var(--text-muted); - white-space: nowrap; -} - -.cumthi-cell { - font-size: 0.82rem; - color: var(--text-muted); - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* --------------------------------------------------------------------------- - Hub route (/thptqg/) — replaces the old standalone static index.html - --------------------------------------------------------------------------- */ - -.hub { - max-width: 42rem; - margin: 3rem auto; - padding: 0 1rem; - line-height: 1.6; -} - -.hub h1 { - font-size: 1.5rem; - margin-bottom: 0.5rem; -} - -.hub-list { - list-style: none; - padding: 0; - margin: 1.5rem 0; - display: grid; - gap: 0.75rem; -} - -.hub-link { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 0.25rem 0.75rem; - padding: 0.9rem 1.1rem; - border: 1px solid var(--border); - border-radius: 10px; - background: var(--surface, transparent); - text-decoration: none; - color: inherit; -} - -.hub-link:hover, -.hub-link:focus-visible { - border-color: var(--border-strong); -} - -.hub-label { - font-weight: 600; -} - -.hub-blurb { - color: var(--text-muted); - font-size: 0.9rem; -} - -.hub-archive h2 { - font-size: 0.95rem; - font-weight: 600; - color: var(--text-muted); - margin-bottom: 0; -} - -.hub-list-compact { - margin-top: 0.75rem; -} - -.hub-back { - margin: 0.35rem 0 0; - font-size: 0.85rem; -} - -.hub-back a { - color: var(--text-muted); - text-decoration: none; -} - -.hub-back a:hover { - text-decoration: underline; -} diff --git a/src/App.jsx b/2017/src/App.jsx similarity index 77% rename from src/App.jsx rename to 2017/src/App.jsx index 930a6c6..94646c7 100644 --- a/src/App.jsx +++ b/2017/src/App.jsx @@ -4,19 +4,17 @@ import { SearchForm } from "./components/search-form"; import { ScoreTable } from "./components/score-table"; import { StudentDetail } from "./components/student-detail"; import { CustomQuery } from "./components/custom-query"; -import { Hub } from "./components/hub"; -import { dbOf } from "./datasets"; -import { resolveRoute } from "./router"; -import { isExamId, normaliseExamId } from "./lib/query-mode"; import "./App.css"; +const DB_URL = import.meta.env.BASE_URL + "thptqg2017.db.gz"; const MAX_RESULTS = 100; +const DB_SIZE_MB = 47; // Strip Vietnamese diacritics: "Nguyễn Bửu Lộc" → "nguyen buu loc" function toAscii(str) { return str .normalize("NFD") - .replace(/[̀-ͯ]/g, "") + .replace(/[\u0300-\u036f]/g, "") .replace(/đ/gi, "d") .toLowerCase(); } @@ -40,11 +38,8 @@ function readUrlQuery() { return new URL(window.location.href).searchParams.get("q") || ""; } -/** The score-lookup view for one dataset. */ -function DatasetApp({ dataset }) { - const { db, loading, error, progress } = useSqlite( - dbOf(dataset, import.meta.env.BASE_URL), - ); +function App() { + const { db, loading, error, progress } = useSqlite(DB_URL); const [results, setResults] = useState(null); const [searchError, setSearchError] = useState(null); const [activeTab, setActiveTab] = useState("search"); @@ -60,14 +55,14 @@ function DatasetApp({ dataset }) { writeUrlQuery(q); try { + const isExamId = /^\d+$/.test(q); let stmt; - if (isExamId(q)) { - // Letter-prefixed 2016 IDs are stored upper-case; digits are unaffected. + if (isExamId) { stmt = db.prepare( "SELECT * FROM student WHERE so_bao_danh = $q LIMIT $limit", ); - stmt.bind({ $q: normaliseExamId(q), $limit: MAX_RESULTS }); + stmt.bind({ $q: q, $limit: MAX_RESULTS }); } else if (isAsciiOnly(q)) { stmt = db.prepare( "SELECT * FROM student WHERE ho_ten_ascii LIKE $q LIMIT $limit", @@ -98,21 +93,17 @@ function DatasetApp({ dataset }) { [db], ); - // Hydrate a ?q= deep link once, as soon as the database is ready. Fires at - // most once per mount, so it cannot cascade. + // Run initial URL query once DB is ready useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect if (db && query) handleSearch(query); // eslint-disable-next-line react-hooks/exhaustive-deps }, [db]); - // Read the candidate count for the footer once the database loads. One-shot - // per mount; the query result cannot change without a new database. + // Fetch total count for footer once DB loads useEffect(() => { if (!db) return; const stmt = db.prepare("SELECT COUNT(*) AS c FROM student"); stmt.step(); - // eslint-disable-next-line react-hooks/set-state-in-effect setTotalCount(stmt.getAsObject().c); stmt.free(); }, [db]); @@ -150,10 +141,9 @@ function DatasetApp({ dataset }) { return (
-

{dataset.title}

-

{dataset.subtitle}

-

- ← Tất cả các kỳ thi +

Tra cứu điểm thi THPT Quốc gia 2017

+

+ Dữ liệu thí sinh toàn quốc · Hỗ trợ truy vấn SQL tùy chỉnh

@@ -161,7 +151,7 @@ function DatasetApp({ dataset }) { {loading && (

- Đang tải cơ sở dữ liệu ~{dataset.dbSizeMb} MB + Đang tải cơ sở dữ liệu ~{DB_SIZE_MB} MB {progress > 0 ? ` · ${progress}%` : ""}

@@ -198,7 +188,6 @@ function DatasetApp({ dataset }) { onSearch={handleSearch} onClear={handleClear} disabled={loading || !!error} - examples={dataset.examples} /> {searchError && ( @@ -220,17 +209,13 @@ function DatasetApp({ dataset }) { )} {activeTab === "sql" && ( - + )}