Files
simonandClaude Opus 5.5 456f9816f4 Redesign the web app from a written design concept
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>
2026-10-28 22:21:44 +01:00

115 lines
5.4 KiB
Markdown
Raw Permalink 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.
# 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).