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

5.4 KiB
Raw Permalink Blame History

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

  1. Create src/app/[locale]/<route>/page.tsx – an async server component built from the shared parts:
    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:

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, 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).