Design concept and design system in docs/design/: principles (evidence
first, neutral, fair to the people shown, plain language, time as a
dimension, accessible and private), information architecture, core
patterns and a concept for every page; tokens with checked contrast,
component catalogue, content and accessibility rules.
web:
- neutral ink-indigo accent instead of flag red; party colours only as
small markers next to the name; light/dark theme with a switcher
- stylesheets split into tokens, base, layout, components, print
- new header (search, filter count, language, theme, mobile menu) and
footer; breadcrumbs, page headers, tabs with counts, filter bars that
collapse on phones, pagination, notices and empty states
- claim cards with source meter, legal stage, innocence notice inside
the card and a permalink; claim pages with citation (/angaben/{id})
- profiles with role timeline and key figures; party and organisation
dashboards; vote list with position chips; calendar agenda on phones;
tables that stack into cards; loading and error pages; favicon
backend:
- GET /api/v1/claims/{id} and MCP tool get_claim for permalinks
- parties list: evaluate person_status once instead of per party
(30 s -> 1 s on the sample database, identical result)
- parliament calendar: HTML-decode titles ("84. Sitzung")
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
5.4 KiB
Markdown
115 lines
5.4 KiB
Markdown
# Extending the web app
|
||
|
||
Next.js 15 (App Router, React Server Components) with next-intl (German default, English). Pages fetch
|
||
from the API on the server; there is almost no client-side JavaScript. How it should look and why:
|
||
[design/README.md](../design/README.md) (concept) and [design/design-system.md](../design/design-system.md)
|
||
(tokens, components, content rules).
|
||
|
||
```
|
||
web/
|
||
messages/de.json, en.json all UI text (keys must exist in both)
|
||
src/app/[locale]/<route>/page.tsx pages (German route names: /personen, /parteien, /geld …)
|
||
src/app/[locale]/layout.tsx frame: header, footer, theme, stylesheets
|
||
src/app/[locale]/loading.tsx, error.tsx, not-found.tsx
|
||
src/app/icon.svg favicon
|
||
src/components/ shared components (catalogue: design-system.md §4)
|
||
src/components/nav.ts site structure for header, menu and footer
|
||
src/styles/ tokens, base, layout, components, print
|
||
src/lib/api.ts typed API client (server-side only; reads the filter cookie)
|
||
src/lib/api-types.ts GENERATED from the backend's response models – do not edit
|
||
src/lib/format.ts dates, periods, numbers, money, party colours
|
||
src/lib/prefs.ts the visitor's source/topic filter and theme (cookies)
|
||
src/lib/roles.ts role labels
|
||
```
|
||
|
||
## Types come from the backend
|
||
|
||
After any change to `backend/politrack/api/schemas.py`:
|
||
|
||
```bash
|
||
cd backend && politrack openapi --ts ../web/src/lib/api-types.ts
|
||
cd ../web && npm run typecheck
|
||
```
|
||
|
||
`api.ts` re-exports the generated types (`Claim`, `MoneyFlow`, …) and adds one function per endpoint.
|
||
`api.claims()`, `api.money()`, `api.social()` … automatically apply the visitor's source filter
|
||
(`withPrefs`), so every list respects "which sources do I trust". `api.claim(id)` deliberately does not:
|
||
a permalink shows the same to everyone.
|
||
|
||
## Adding a page
|
||
|
||
1. Create `src/app/[locale]/<route>/page.tsx` – an async server component built from the shared parts:
|
||
```tsx
|
||
export async function generateMetadata({params}: Props): Promise<Metadata> {
|
||
const {locale} = await params;
|
||
const t = await getTranslations({locale, namespace: 'MyPage'});
|
||
return {title: t('title')};
|
||
}
|
||
|
||
export default async function Page({params, searchParams}: Props) {
|
||
const {locale} = await params;
|
||
setRequestLocale(locale);
|
||
const sp = await searchParams;
|
||
const t = await getTranslations('MyPage');
|
||
const page = Math.max(1, Number(sp.page) || 1);
|
||
const rows = await api.something({kind: sp.kind, limit: PAGE, offset: (page - 1) * PAGE});
|
||
return (
|
||
<>
|
||
<PageHeader title={t('title')} lede={t('intro')} />
|
||
<FilterBar id="mypage" active={countActive(sp.kind)} resetHref="/meine-seite">
|
||
<Field label={t('kind')}><select name="kind" defaultValue={sp.kind ?? ''}>…</select></Field>
|
||
</FilterBar>
|
||
{rows.length === 0 ? <EmptyState>{t('none')}</EmptyState> : <ul className="grid-cards">…</ul>}
|
||
<Pagination pathname="/meine-seite" query={sp} page={page} hasNext={rows.length === PAGE} />
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
Filters are plain GET forms – shareable URLs, no client state.
|
||
2. Add the texts to **both** `messages/de.json` and `messages/en.json`.
|
||
3. Add it to the site structure in `src/components/nav.ts` (primary, "Mehr" or a footer group) or link it
|
||
from the home page's "Entdecken" cards.
|
||
4. Page modules may only export Next.js page fields (`default`, `generateMetadata`, …); keep helpers and
|
||
constants module-local or in `src/components`.
|
||
5. Check it at 390 px and in both themes (design-system.md §7).
|
||
|
||
## Showing a new claim type
|
||
|
||
Claims render through `ClaimList` → `ClaimCard`; type-specific details come from the registry in
|
||
`src/components/ClaimDetails.tsx`:
|
||
|
||
```tsx
|
||
const ambassador: Renderer = (claim, {t}) => (
|
||
<p className="claim-detail">{t('country')}: {String(claim.data.country ?? '')}</p>
|
||
);
|
||
|
||
const DETAILS: Partial<Record<ClaimType, Renderer[]>> = {
|
||
…,
|
||
career: [counterpart, ambassador],
|
||
};
|
||
```
|
||
|
||
Also add an icon to `TYPE_ICONS` in `src/components/badges.tsx`, the label under
|
||
`Claims.type.<claim_type>` in both message files, and (if it should appear) the type to the person page's
|
||
`TABS`.
|
||
|
||
## Non-negotiables in the UI
|
||
|
||
* **Every fact shows its sources** (the evidence disclosure under each claim, source chips in tables).
|
||
Don't render a claim without its evidence.
|
||
* **Presumption of innocence:** `needsInnocenceNotice()` in `ClaimList.tsx` decides; the notice stays inside
|
||
the claim card. Never remove it for anything but an undisputed final conviction.
|
||
* **Connections are not accusations:** sections that show networks or money of connected organisations
|
||
carry the note (`Network.note`, `Organizations.connectionNote`).
|
||
* **Neutral colours:** party colours only through `partyStyle()` as dots and thin bars, next to the name.
|
||
* Unverified social accounts are marked (Wikidata-only handles).
|
||
* No third-party scripts, fonts, embeds or trackers.
|
||
|
||
## Styling
|
||
|
||
Global CSS with design tokens, one section per component in `src/styles/components.css`. Use tokens
|
||
(`var(--ink-2)`, `var(--space-4)`) instead of raw values, reuse the components and layout utilities
|
||
listed in [design-system.md](../design/design-system.md), and add a new section (and a row in its
|
||
component table) when you create a component. A new token goes into all three blocks of `tokens.css`
|
||
(light, dark by preference, dark by choice).
|