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>
5.4 KiB
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 (concept) and 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:
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
- Create
src/app/[locale]/<route>/page.tsx– an async server component built from the shared parts:Filters are plain GET forms – shareable URLs, no client state.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} /> </> ); } - Add the texts to both
messages/de.jsonandmessages/en.json. - 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. - Page modules may only export Next.js page fields (
default,generateMetadata, …); keep helpers and constants module-local or insrc/components. - 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:
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()inClaimList.tsxdecides; 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, 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).