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>
This commit is contained in:
simonandClaude Opus 5.5 committed 2026-10-28 22:21:44 +01:00
1 parent 45514624f4
commit 456f9816f4
76 files changed
+7127 -1280

No files matched your search

+55 -22
View File
@@ -1,17 +1,25 @@
# 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.
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/components/ shared server components
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, money, localized names
src/lib/prefs.ts the user's source/topic filter (cookie)
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
@@ -25,34 +33,54 @@ 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".
(`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:
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 rows = await api.something({...});
return <>…</>;
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 `<form method="get">` – shareable URLs, no client state.
Filters are plain GET forms – shareable URLs, no client state.
2. Add the texts to **both** `messages/de.json` and `messages/en.json`.
3. Link it from the navigation (`src/app/[locale]/layout.tsx`) or the home page's "Entdecken" cards.
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`; type-specific details come from the registry in
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 small">{t('country')}: {String(claim.data.country ?? '')}</p>
<p className="claim-detail">{t('country')}: {String(claim.data.country ?? '')}</p>
);
const DETAILS: Partial<Record<ClaimType, Renderer[]>> = {
@@ -61,21 +89,26 @@ const DETAILS: Partial<Record<ClaimType, Renderer[]>> = {
};
```
Also add the label under `Claims.type.<claim_type>` in both message files, and (if it should appear) the
type to the person page's `TABS`.
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 "Quellen" disclosure under each claim, `[source]` links in
tables). Don't render a claim without its evidence.
* **Presumption of innocence:** `needsInnocenceNotice()` in `ClaimList.tsx` decides; never remove the
notice for anything but an undisputed final conviction.
* **Connections are not accusations:** pages that show networks or money of connected organisations
* **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
One global stylesheet (`src/app/globals.css`) with CSS variables for light/dark mode. Reuse the existing
classes (`claims`, `claim-detail`, `chip`, `tag`, `notice`, `grid-2`, `compact`, `num`, `muted`, `small`)
before adding new ones.
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).