Added the frontend in web/, following its code conventions, with linting and generated backend types
web/CODE_CONVENTIONS.md covers the Vite, SolidJS, Tailwind and TypeScript frontend: tooling, files, imports, exports, naming, TypeScript, formatting, Solid's reactivity, styling, the backend's types, API calls, errors, security, formatting values, comments, accessibility, storage, dependencies and tests. npm run check (strict tsc -b, ESLint and Prettier) passes, and npm run build builds it. ESLint (typescript-eslint with type information, eslint-plugin-solid, simple-import-sort, eslint-comments, and rules for the naming, type import, export type and promise conventions) and Prettier (with the Tailwind class plugin) check what they can. Tool versions are pinned exactly. Imports between folders use @/, which works in Vite as well as TypeScript. The backend's types are generated from its OpenAPI description into src/types/api.ts by npm run generate:api, which turns swag's x-nullable into null types first; src/types/Opportunities.ts names them for the app. The app works against the backend's API: it fetches every page of /api/opportunities with each opportunity's marks, the classifications to filter by from /api/classifications, sets and clears marks (hidden among them) with the CSRF token, and shows the OCDS tender statuses. Its lists' requests are aborted when reloaded or no longer needed. The dev server proxies /api and /ws to job-search serve, by default at localhost:9393. web/go.mod keeps web/ out of the Go module, as an npm package in node_modules ships Go code. The page stays up to date over the backend's WebSocket (src/api/events.ts, with hand-written event types in src/types/Events.ts), reconnecting with a growing delay. ClassificationsUpdated changes the filter's classifications in place and OpportunityMarksUpdated sets an opportunity's marks (unless this page is still saving its own change). OpportunitiesUpdated catches the list up: it asks for the opportunities changed after the last change batch it has seen (changed_after) and merges them in, reloading the whole list only when the backend doesn't know the batch. A reconnect catches up the same way and reloads the classifications.
This commit is contained in:
1 parent
9905ffb4cb
commit
1832ea7437
42 files changed
+8019
No files matched your search
@@ -0,0 +1,96 @@
|
||||
# JobSearch - Frontend
|
||||
|
||||
A Vite + SolidJS + Tailwind CSS (v4) + TypeScript table of opportunities with
|
||||
title search, status, mark and classification filters, and per-opportunity
|
||||
mark and hide controls.
|
||||
|
||||
## Run
|
||||
|
||||
npm ci
|
||||
npm run dev
|
||||
|
||||
The dev server proxies `/api` to the backend, `job-search serve`, at its
|
||||
default address (`http://localhost:9393`). To use another, copy `.env.example`
|
||||
to `.env` and set `API_PROXY_TARGET`. `VITE_API_BASE_URL` changes the `/api`
|
||||
prefix for production builds.
|
||||
|
||||
Before committing, `npm run check` type-checks, lints (ESLint) and checks
|
||||
formatting (Prettier); `npm run format` formats everything. See
|
||||
[CODE_CONVENTIONS.md](CODE_CONVENTIONS.md).
|
||||
|
||||
The backend's types in `src/types/api.ts` are generated from its OpenAPI
|
||||
description: after changing the Go types or handlers, run `go generate ./...`
|
||||
in the repository, then `npm run generate:api`.
|
||||
|
||||
## API
|
||||
|
||||
The backend describes its API at `/api/openapi.json`, viewable at `/api/docs`.
|
||||
The frontend uses, through `src/api/client.ts`:
|
||||
|
||||
| Method | Path | Response |
|
||||
| ------ | ------------------------------------------ | ---------------------------------------------- |
|
||||
| GET | `/api/opportunities?offset=&limit=` | A page of opportunities, with their marks |
|
||||
| GET | `/api/opportunities?changed_after={batch}` | The opportunities changed since a change batch |
|
||||
| GET | `/api/classifications` | Every classification in use, by code |
|
||||
| PUT | `/api/opportunities/{uuid}/marks/{mark}` | Sets one mark; returns all the marks |
|
||||
| DELETE | `/api/opportunities/{uuid}/marks/{mark}` | Clears one mark; returns all the marks |
|
||||
| GET | `/api/csrf` | The CSRF token PUT and DELETE need |
|
||||
|
||||
The page also listens on the backend's WebSocket (`/ws`, through
|
||||
`src/api/events.ts`), so it stays up to date without polling:
|
||||
|
||||
| Event | The page |
|
||||
| ------------------------- | -------------------------------------------------------------- |
|
||||
| `ClassificationsUpdated` | Adds and removes classifications in the filter, in place |
|
||||
| `OpportunityMarksUpdated` | Shows the opportunity's new marks (unless it's saving its own) |
|
||||
| `OpportunitiesUpdated` | Catches up: fetches what changed and merges it in |
|
||||
|
||||
If the connection drops, it reconnects (waiting 1s, doubling up to 30s),
|
||||
catches the opportunities up and reloads the classifications, as it may have
|
||||
missed events.
|
||||
|
||||
Catching up: every page of opportunities, and every `OpportunitiesUpdated`
|
||||
event, names the backend's newest change batch (a hash). The page remembers
|
||||
the batch it's up to date with, and asks for everything changed after it
|
||||
(`changed_after`): opportunities that are new, have new releases or had
|
||||
their marks changed. If the backend doesn't know the batch (410), or has
|
||||
recorded none yet, the whole list is reloaded instead.
|
||||
|
||||
The list is fetched a page (100) at a time until there are no more. The marks
|
||||
are `interested`, `skipped`, `bid_submitted`, `lost`, `won` and `hidden`;
|
||||
Won and Lost exclude each other (409). The classifications are the ones the
|
||||
filter offers: only those some opportunity has. The client fetches the CSRF
|
||||
token when it first needs it, and a new one if a request is refused for it.
|
||||
|
||||
## Where things are
|
||||
|
||||
- `src/api/client.ts` – every request to the backend
|
||||
- `src/api/events.ts` – the WebSocket connection and its events
|
||||
- `src/types/api.ts` – the backend's types, generated (don't edit)
|
||||
- `src/types/Opportunities.ts` – the app's names for them, and the known
|
||||
statuses
|
||||
- `src/types/Annotations.ts` – the marks, their labels, and an opportunity's
|
||||
marks as the UI uses them
|
||||
- `src/primitives/createOpportunities.ts` – loads the opportunities (and
|
||||
catches them up) and the classifications
|
||||
- `src/primitives/createLiveUpdates.ts` – applies the backend's events
|
||||
- `src/primitives/createOpportunityAnnotations.ts` – changing marks and hiding
|
||||
(applied at once, undone if the save fails)
|
||||
- `src/lib/filterOpportunities.ts` – search and filter logic
|
||||
- `src/lib/opportunities.ts` – merging changed opportunities into the list
|
||||
- `src/components/` – FilterPanel, ClassificationPicker, OpportunityTable,
|
||||
MarkMenu, ThemeToggle; `styles.ts` holds shared Tailwind class strings
|
||||
|
||||
Theme colours and the font are set in the `@theme` block of `src/index.css`.
|
||||
|
||||
## Saved preferences
|
||||
|
||||
Two preferences are kept in the browser's localStorage (`src/lib/storage.ts`):
|
||||
|
||||
- `job_search.theme`: `light` or `dark`. If nothing is saved, the app follows
|
||||
the OS setting. A small script in `index.html` applies the saved theme
|
||||
before the first paint, so there's no flash.
|
||||
- `job_search.filtersOpen`: whether the filter panel is expanded.
|
||||
|
||||
Dark mode works by swapping the colour tokens under `.dark` in
|
||||
`src/index.css`, so components don't need `dark:` variants.
|
||||
Reference in new issue
Block a user