Files
job-search/web/README.md
T
bwilliams f8648d765a Added a dialog with each opportunity's details and releases
Every row has a details icon under the Find a Tender link. It opens a
dialog with the opportunity's details from the list, and fetches the
opportunity with its releases, shown as tabs, newest first. Each tab
summarises the release's tender, awards and contracts, links to its
Find a Tender notice where it is one, and has the release's JSON,
pretty-printed and highlighted, in a section laid out only when it's
opened.

The JSON is highlighted by splitting it into tokens shown as text, not
HTML, as releases come from outside sources. The README's saved
preferences now include the filters.
2026-10-19 10:37:27 +01:00

119 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/opportunities/{uuid}` | One opportunity, with its releases |
| 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.
Both list requests send `released_from` (an RFC 3339 time), so only the
opportunities published in the last few days the filter names (7 by default,
from midnight UK time) are loaded; changing the number of days loads the list
again.
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 every one some
stored opportunity has; the filter offers only those the loaded opportunities
(the days window) have, and any already chosen. 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,
and Find a Tender notice links
- `src/lib/ocdsRelease.ts` – what the details dialog reads from a release's
OCDS JSON
- `src/lib/jsonTokens.ts` – splits JSON into tokens for highlighting
- `src/components/` – FilterPanel, ClassificationPicker, OpportunityTable,
MarkMenu, OpportunityDialog (an opportunity's details), ReleaseTabs,
JsonView, 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
Three 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.
- `job_search.filters`: the filters, as JSON. A field that isn't valid is
read as its default.
Dark mode works by swapping the colour tokens under `.dark` in
`src/index.css`, so components don't need `dark:` variants.