mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-10-19 10:15:19 +00:00
refactor(web): rebuild the frontend on SvelteKit, TypeScript and Tailwind
The app was a single-entry React SPA: one index.html that the assembler copied to every dataset path, with a hand-rolled router resolving the dataset from window.location and the title patched in at runtime because one file had to serve every route. SvelteKit prerenders a real page per route instead. The entry generator in routes/[dataset]/+page.ts reads the same datasets.json the assembler does, so the set of pages and the set of databases cannot drift apart, and each dataset page ships its own <title> and description. Everything framework-free moved across unchanged in behaviour and became typed: the admission blocks, subject list, query classifier and SQL presets. `Student` now mirrors the 22-column table, so a mistyped column name fails the build rather than rendering blank. Tailwind replaces the stylesheet. Theme tokens are CSS variables, which keeps dark mode a single block of overrides rather than a `dark:` variant on every class. The score tiers stay hand-written CSS: the class is chosen at runtime from a score, and no utility generator can see that. Adds the tests the frontend never had, over the three modules where a silent wrong answer is possible — most importantly that toAscii here folds exactly as ToAscii does in the parser, which is what makes accent-insensitive search find anything. The assembler stops copying index.html per dataset and checks that the build prerendered each one instead. The database presence, raw-artifact and idempotence guards are untouched. Verified: 25 tests, ESLint and svelte-check clean, and a full `assemble site` producing /thptqg/, /thptqg/2016/, /thptqg/2017/ and 404.html with absolute asset URLs. Not verified in a browser — this machine has none.
This commit is contained in:
1 parent
e2112fdb38
commit
3fd137a233
49 files changed
+4432
-3112
No files matched your search
@@ -11,7 +11,7 @@ One-time setup: **Settings → Pages → Source: GitHub Actions**.
|
||||
2. Parser, crawler and assembler test suites, web lint, `govulncheck` over all three modules
|
||||
3. `go -C assembler run ./cmd/assemble` — the whole pipeline: compile the
|
||||
parser, build and verify each database, compress it into `.build/public/db/`,
|
||||
run the Vite build, assemble `_site/`
|
||||
run the web build, assemble `_site/`
|
||||
4. `actions/upload-pages-artifact` + `actions/deploy-pages`
|
||||
|
||||
The database build dominates the runtime: roughly 348 MB of Excel is parsed on
|
||||
@@ -46,9 +46,9 @@ go -C assembler run ./cmd/assemble site
|
||||
|
||||
## Base path
|
||||
|
||||
`vite.config.js` sets `base: "/thptqg/"`. If you fork under a different repo
|
||||
name, update it to match — assets are referenced absolutely, so a mismatch shows
|
||||
up as a blank page with 404s on `/assets/...`.
|
||||
`svelte.config.js` sets `paths.base: "/thptqg"`. If you fork under a different
|
||||
repo name, update it to match — assets are referenced absolutely, so a mismatch
|
||||
shows up as a blank page with 404s on `/_app/...`.
|
||||
|
||||
## Adding a dataset
|
||||
|
||||
@@ -96,9 +96,9 @@ run rebuilds the older state. There is no data to migrate.
|
||||
|
||||
| Symptom | Typical cause |
|
||||
| --- | --- |
|
||||
| Blank page, 404 on assets | `base` in `vite.config.js` does not match the repo name |
|
||||
| Blank page, 404 on assets | `paths.base` in `svelte.config.js` does not match the repo name |
|
||||
| `Failed to fetch database: 404` | Dataset id in `datasets.json` does not match the file in `db/` |
|
||||
| A route 404s | The site step did not run, or the id is missing from `datasets.json` |
|
||||
| WASM fails to load | `sql.js.org` unreachable — self-host `sql-wasm.wasm` and update `SQL_WASM_URL` in `use-sqlite.js` |
|
||||
| WASM fails to load | `sql.js.org` unreachable — self-host `sql-wasm.wasm` and update `SQL_WASM_URL` in `lib/sqlite.svelte.ts` |
|
||||
| Deploy fails on assembly | An uncompressed database artefact reached the output; the error names the files |
|
||||
| Missing rows after a data update | Unknown Excel header — check the per-file row counts the parser prints |
|
||||
+20
-20
@@ -20,7 +20,7 @@ data/<id>/*.xls(x)
|
||||
▼ assembler/ — row count must match datasets.json, then gzip
|
||||
.build/public/db/<id>.db.gz (the raw .db does not survive)
|
||||
│
|
||||
▼ assembler/ → vite build (root = web/, publicDir = .build/public)
|
||||
▼ assembler/ → npm run build (SvelteKit static, assets = .build/public)
|
||||
web/dist/
|
||||
│
|
||||
▼ assembler/ — one index.html per dataset; every database must be present
|
||||
@@ -40,7 +40,7 @@ data/2017/ → parser/configs/2017.yml → db/2017.db.gz → /thptqg/2017/
|
||||
|
||||
`datasets.json` at the repository root declares the ids once, with the row count
|
||||
and artifact size the assembler enforces. It is JSON rather than a module
|
||||
because the assembler is a Go program and the Vite app is not, and JSON is the
|
||||
because the assembler is a Go program and the web app is not, and JSON is the
|
||||
only format both parse without a dependency.
|
||||
|
||||
Presentation — titles, labels, search examples, SQL presets — stays in
|
||||
@@ -100,23 +100,21 @@ URLs are flat, one segment per dataset, and the segment is the id:
|
||||
/thptqg/2017/
|
||||
```
|
||||
|
||||
`web/src/router.js` is an exact match on that segment. The nested form used
|
||||
before (`/thptqg/2017/old/`) would have needed longest-prefix matching, since it
|
||||
also starts with `/thptqg/2017/`. Both of those URLs addressed the two removed
|
||||
2017 archives, so the rewrite that kept them working has gone with them; they
|
||||
fall through to the hub like any other unknown path.
|
||||
The route is `web/src/routes/[dataset]/`, and its entry generator reads the same
|
||||
`datasets.json` the assembler does, so the set of pages and the set of databases
|
||||
cannot drift apart. Unknown paths fall through to the hub.
|
||||
|
||||
A single Vite build emits one `index.html`. Because `base` is absolute
|
||||
(`/thptqg/`), that file references `/thptqg/assets/...` regardless of the
|
||||
directory it is served from, so the assemble step copies it to every route and
|
||||
each URL is a real static file. **No SPA 404-fallback redirect is used** — the
|
||||
usual hack rewrites URLs and would interfere with the deep links.
|
||||
SvelteKit prerenders one HTML file per route, each with its own `<title>`.
|
||||
Asset URLs stay absolute (`paths.relative: false`), so the copy of the hub that
|
||||
serves as `404.html` resolves its assets from any depth. **No SPA 404-fallback
|
||||
redirect is used** — the usual hack rewrites URLs and would interfere with the
|
||||
deep links.
|
||||
|
||||
## Serving both exam years without branching
|
||||
|
||||
No component contains a per-dataset conditional. Two mechanisms do the work:
|
||||
|
||||
- **All-NULL columns are hidden.** `score-table.jsx` drops any column where
|
||||
- **All-NULL columns are hidden.** `score-table.svelte` drops any column where
|
||||
every row in the result set is NULL, so 2016 rows surface Cụm thi / GT / Đức /
|
||||
Nhật and 2017 rows surface KHTN / KHXH / GDCD / Nga.
|
||||
- **Incomplete admission blocks are skipped.** `computeBlocks()` only returns a
|
||||
@@ -125,13 +123,14 @@ No component contains a per-dataset conditional. Two mechanisms do the work:
|
||||
self-exclude wherever those languages were not sat.
|
||||
|
||||
Anything genuinely per-dataset — title, source, database size, search examples,
|
||||
SQL presets — lives in `web/src/datasets.js`.
|
||||
SQL presets — lives in `web/src/lib/datasets.ts`.
|
||||
|
||||
## Exam ID formats
|
||||
|
||||
`web/src/lib/query-mode.js` decides whether a query is an exam ID or a name, and is
|
||||
shared by `App.jsx` and `search-form.jsx` (they previously held separate copies
|
||||
and had drifted apart on exactly this rule).
|
||||
`web/src/lib/query-mode.ts` decides whether a query is an exam ID or a name, and
|
||||
is shared by the dataset page and `search-form.svelte` (they previously held
|
||||
separate copies and had drifted apart on exactly this rule). `query-mode.test.ts`
|
||||
covers every form in the table below.
|
||||
|
||||
| Form | Example | Where |
|
||||
| --- | --- | --- |
|
||||
@@ -175,16 +174,17 @@ total descending.
|
||||
| Diacritics search | Pre-computed `ho_ten_ascii` | `LOWER(REPLACE(...))` at query time defeats the index |
|
||||
| SQL safety | Leading-keyword allowlist | `sql.js` is in-memory so writes cannot persist; the allowlist prevents confusion |
|
||||
| Row caps | 100 (lookup), 1000 (SQL) | Keeps DOM render sizes reasonable |
|
||||
| Routing | Hand-rolled, ~15 lines | Three static routes do not justify a router dependency |
|
||||
| Routing | SvelteKit file routes, prerendered | Each dataset gets a real HTML file with its own title |
|
||||
| Styling | Tailwind, with tier colours as CSS variables | Tier classes are chosen at runtime, which no utility generator can see |
|
||||
|
||||
## Risks and limitations
|
||||
|
||||
- **Database size.** 44–48 MB gzipped per dataset; slow links wait, mitigated by
|
||||
- **Database size.** 45–48 MB gzipped per dataset; slow links wait, mitigated by
|
||||
a progress bar.
|
||||
- **Browser memory.** The full database lives in RAM; older mobile devices may
|
||||
run out.
|
||||
- **`sql.js.org` dependency.** If that CDN is unreachable, the WASM fails to
|
||||
load. Self-hosting `sql-wasm.wasm` and updating `SQL_WASM_URL` in
|
||||
`web/src/hooks/use-sqlite.js` is the fix.
|
||||
`web/src/lib/sqlite.svelte.ts` is the fix.
|
||||
- **Excel format drift.** A new source file with an unseen header layout needs a
|
||||
new branch in `parser/internal/ingest/detect2016.go` or a new config.
|
||||
Reference in new issue
Block a user