Files
Calendar_Frontend/docs/architecture.md
T
PaddyandClaude Opus 5.5 0cda40f85e Rewrite the calendar frontend in Next.js
Replaces the Angular 18 app with Next.js 16 / React 19 / Tailwind 4 / shadcn,
the same stack and design system as nachklang-feedback, planned in
docs/rewrite-plan.md.

- Admin: all calendars in one mobile-friendly list (month groups, calendar
  chips, Kommend/Vergangen/Alle, search, sort, filters in the URL), an edit
  sheet for create/edit/duplicate/delete, publish confirmation, and an
  "Abonnieren" dialog with subscription links for every calendar.
- Public landing page: subscribe links from env instead of hardcoded, plus
  the next public events, server-rendered and revalidated every 5 minutes.
- All times handled explicitly as Europe/Berlin; whole-day events stored as
  00:00-23:59. Fixes carried over from the Angular app: the move dialog
  leaking a cancelled calendar change, saves that looked saved when they
  failed, in-place Date mutation, and duplicated calendar-id mappings.
- Same shared-session auth as before (proxy.ts gate, /admin/me, 401 vs 403).
- 112 Vitest tests over lib/; docs in docs/ (architecture, features,
  gotchas, API wish list, deployment).

Needs the API branch feature/calendar-ical-escaping-subscriptions deployed
first (GET /calendar/events/subscriptions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:15:59 +02:00

5.4 KiB

Architecture

Next.js 16 (App Router) + React 19 + TypeScript + Tailwind 4 + shadcn (preset b7ClO5que, Base UI primitives) - the same stack as nachklang-feedback. Rewritten from Angular in September 2026; the decisions behind it are in rewrite-plan.md.

Routes

Path What Rendering
/ Public landing page: subscribe links + next public events Server component, static, revalidate = 300
/admin Event management (list + edit sheet) Client, behind proxy.ts and the /me gate
/admin/login Forwarder to the admin app's login Client

Authentication

This app has no login and no credential of its own. The session is an httpOnly cookie (__Secure-nachklang.session_token in production, nachklang.session_token over http in dev) that the API sets on .nachklang.art; it is shared by calendar, feedback, tickets and admin.

  1. proxy.ts - a convenience gate on /admin/*: no session cookie → /admin/login?redirect=…. It only sees the cookie's presence, never its validity.
  2. app/admin/login/page.tsx forwards to ${NEXT_PUBLIC_ADMIN_APP_URL}/login?redirect=<absolute URL>.
  3. app/admin/layout.tsx calls GET /admin/me and decides:
    • 401 → login (the only status that redirects);
    • 403, or 200 without "calendar" in apps → "Kein Zugriff" (no redirect: it would loop);
    • anything else (5xx, network, CORS = status 0) → "nicht erreichbar" (no redirect).
  4. Every API call uses credentials: "include" (lib/api.ts). Without it the browser sends no cookie and the API answers 401.
  5. Any 401 later (session ended in another app) → toast + login with this page as return target.

GET /admin/me answers 200 for an account without the calendar permission; only apps shows it. The layout's apps.includes("calendar") check is load-bearing.

Data flow (admin)

  • components/admin/useEvents.ts loads all five calendars in parallel from GET /calendar/events/{slug}/json (there is no all-in-one endpoint). Each calendar loads and fails on its own; a per-calendar request counter drops stale responses. Deleted events (the admin feed returns them) are dropped by parseApiEvent.
  • Filtering, sorting and month grouping are client-side (lib/filter.ts); nothing is re-fetched when a filter changes. The view state lives in the URL (lib/url-state.ts, useListState): ?cal=public,choir&range=past&sort=start_desc&q=probe, defaults omitted. The default sort depends on the range (Vergangen = newest first).
  • Writes (components/admin/EventSheet.tsx):
    • validate (validateEventForm) → focus the first invalid field, no request;
    • new → POST /calendar/events;
    • edit → PUT /calendar/events/move/:id if the calendar changed, then PUT /calendar/events/:id only if anything else changed (contentChanged);
    • the sheet closes only after the API answered; a failure keeps the edits and shows the reason.
    • After a save the list updates at once (eventFromPayload), then the affected calendars are re-fetched for server-side fields.
  • PUT replaces the whole event and defaults missing fields (status ?? 'PUBLIC', repeatFrequency ?? ''). Every write sends the complete EventPayload - including the quick "Veröffentlichen", which uses payloadFromEvent.

Time

All wall-clock times mean Europe/Berlin, whatever the browser's zone (lib/dates.ts, @date-fns/tz). Forms hold "YYYY-MM-DD" / "HH:mm" strings; the API gets UTC ISO instants.

  • Whole-day events are stored as 00:00 on the first day to 23:59 on the last (inclusive), Berlin. The API's iCal export treats the end date as inclusive and adds a day.
  • Repeating events (any repeatFrequency) count as current and sort by their next occurrence (nextOccurrence); a 29 February birthday falls on 28 February in common years.
  • Vitest runs with TZ=America/New_York so Berlin handling can't pass by accident.

Calendars

lib/calendars.ts is the single list: id, slug, German label, colours, default visibility and a forced recurrence (Birthdays → YEARLY). It mirrors calendarNames in the API's events.router.ts; a new calendar needs an entry in both.

Public landing page

app/page.tsx fetches GET /calendar/events/public/json on the server (lib/public-calendar.ts, next: { revalidate: 300 }), anonymously. On any failure the events section is simply left out; the subscribe links never depend on the API. Links from the database are only rendered if they pass normaliseUrl (http/https) - old rows were never validated.

Subscription URLs for the internal calendars carry the shared password and come from GET /calendar/events/subscriptions (editors only), shown in SubscriptionsDialog.

Configuration

Variable When Purpose
NEXT_PUBLIC_API_BASE_URL build time (inlined) API origin for the browser and the subscribe links
NEXT_PUBLIC_ADMIN_APP_URL build time (inlined) Where signing in happens
API_BASE_URL runtime, optional Server-side fetch of the landing page's events; may be internal

lib/env.ts throws on a missing NEXT_PUBLIC_* value, so a misconfigured build fails instead of shipping localhost URLs.

Tests

npm test - Vitest over the pure logic in lib/ (dates, form rules, filtering, URL state, parsing, API client with a stubbed fetch, landing page selection). Components are checked in the browser, not unit-tested (decision Q10).