Files
Calendar_Frontend/docs/gotchas.md
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

4.5 KiB

Gotchas

Things that turned out differently than expected, so the same wrong assumption isn't made twice. Collected during the rewrite (September 2026); the progress log in rewrite-plan.md has the context for each.

The API

  • PUT replaces the whole event. Missing fields fall back to status: 'PUBLIC' and repeatFrequency: ''. Always send the full EventPayload, even to change one field.
  • "0 rows were updated" comes back as HTTP 200. api.updateEvent turns that body into an error.
  • The API returns raw DB values. wholeDay is 0/1 (observed), and a deleted event's latest version is a row with every field null except status: 'DELETED'. parseApiEvent handles both.
  • GET /admin/me answers 200 for accounts without the calendar permission. Only apps says so.
  • The admin feed ignores includes_calendars; the public/iCal feeds follow it. So an event can appear in a calendar's subscription although it belongs to another calendar.
  • PRIVATE is in the status enum but nothing uses it. The app shows it read-only and keeps it.
  • The iCal times depend on the API process's time zone (formatted with local getHours(), labelled Europe/Berlin). Inferred from the code; not checked on the server.
  • Only the admin DB pool reads DB_PORT. A local calendar test database has to listen on 3306.

Time

  • Historical DST. Until 1995 German summer time ended in September, so 1994-10-02T22:00Z is 23:00 on 2 October in Berlin, not midnight on the 3rd. The date code gets it right; hand-written test data easily doesn't.
  • DST edges in forms. A nonexistent spring time (02:30) becomes 03:30 CEST; an ambiguous autumn time resolves to the second occurrence (CET). Pinned in lib/dates.test.ts.

Next.js / hosting

  • Standalone output doesn't serve .next/static by itself. Without the copy, CSS and JS are 404s. The postbuild script copies it into .next/standalone/.next/static; there is no public/ folder to copy (the favicon lives in app/). Verified by running .next/standalone/server.js.
  • NEXT_PUBLIC_* values are inlined at build time. They must be set when npm run build runs, not only when the server starts; lib/env.ts fails the build if they're missing.
  • The build prerenders / by calling the API. If the build machine can't reach it, the build still succeeds and the events section appears after the first revalidation (≤ 5 min).
  • With revalidate, a short API outage doesn't blank the landing page's events in a running server - Next keeps serving the cached fetch.
  • The dev server must run on port 4200. That's the origin the API and the admin app trust; another port breaks sign-out and the return redirect.
  • Dev runs React strict mode, so every calendar is fetched twice on load in development.
  • A leftover Angular node_modules makes npm install fail on a vite peer conflict.
  • npm 11 blocks install scripts by default (sharp, unrs-resolver didn't run theirs). Lint and build work without them; the app doesn't use next/image.
  • Vitest 5 needs @types/node ≥ 22; the config is vitest.config.mts because package.json has no "type": "module".

shadcn / Base UI

  • npx shadcn add imports cn from the npm package cn and adds it (plus next-themes for the toaster). This repo uses @/lib/utils like feedback, and toasts are light-only - fix both after every shadcn add.
  • Generated components have English labels ("Close"); translated to "Schließen" in dialog.tsx and sheet.tsx.
  • Base UI, not Radix: toggles expose aria-pressed (not data-state=on), triggers take a render prop (not asChild), menu radio items stay open unless closeOnClick is set.
  • Base UI Select ignores synthetic click()s; automated tests need real pointer or keyboard input.
  • Closed dialogs stay in the DOM with data-closed until their exit animation ends - in a hidden tab that never happens. Query [data-open] when checking what's open.
  • In a hidden tab matchMedia change events don't fire either, so a resize there doesn't switch the sheet between bottom and side. It works in a visible window.

CSS

  • Responsive rules inside a capped panel need container queries, not viewport breakpoints (the edit sheet's footer labels use @container / @md:).
  • An implicit grid column grows to its longest unbreakable content (a URL) and defeats truncate; use grid-cols-[minmax(0,1fr)].
  • break-all splits ordinary words mid-word ("App-s"); only use it on text without spaces.