105 lines
5.3 KiB
Markdown
105 lines
5.3 KiB
Markdown
# 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.
|
||
|
||
`npm run build` type-checks and builds the app into `dist/`. `npm run sync`
|
||
then copies `dist/` to `../internal/frontend/dist/`, where the backend embeds
|
||
it: the next Go build of `job-search` serves it. The copy isn't committed.
|
||
|
||
Before committing, `npm run check` type-checks, lints (ESLint), checks
|
||
formatting (Prettier) and runs the tests (Vitest); `npm run format` formats
|
||
everything, and `npm run test:watch` reruns the tests as files change. 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/primitives/createToasts.ts` – the toasts errors are shown in, bottom
|
||
right, for 15 seconds or until dismissed
|
||
- `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, ToastList; `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.
|