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>
4.5 KiB
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'andrepeatFrequency: ''. Always send the fullEventPayload, even to change one field. - "0 rows were updated" comes back as HTTP 200.
api.updateEventturns that body into an error. - The API returns raw DB values.
wholeDayis0/1(observed), and a deleted event's latest version is a row with every fieldnullexceptstatus: 'DELETED'.parseApiEventhandles both. GET /admin/meanswers 200 for accounts without the calendar permission. Onlyappssays 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. PRIVATEis 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:00Zis 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/staticby itself. Without the copy, CSS and JS are 404s. Thepostbuildscript copies it into.next/standalone/.next/static; there is nopublic/folder to copy (the favicon lives inapp/). Verified by running.next/standalone/server.js. NEXT_PUBLIC_*values are inlined at build time. They must be set whennpm run buildruns, not only when the server starts;lib/env.tsfails 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_modulesmakesnpm installfail on a vite peer conflict. - npm 11 blocks install scripts by default (
sharp,unrs-resolverdidn't run theirs). Lint and build work without them; the app doesn't usenext/image. - Vitest 5 needs
@types/node≥ 22; the config isvitest.config.mtsbecausepackage.jsonhas no"type": "module".
shadcn / Base UI
npx shadcn addimportscnfrom the npm packagecnand adds it (plusnext-themesfor the toaster). This repo uses@/lib/utilslike feedback, and toasts are light-only - fix both after everyshadcn add.- Generated components have English labels ("Close"); translated to "Schließen" in
dialog.tsxandsheet.tsx. - Base UI, not Radix: toggles expose
aria-pressed(notdata-state=on), triggers take arenderprop (notasChild), menu radio items stay open unlesscloseOnClickis set. - Base UI Select ignores synthetic
click()s; automated tests need real pointer or keyboard input. - Closed dialogs stay in the DOM with
data-closeduntil their exit animation ends - in a hidden tab that never happens. Query[data-open]when checking what's open. - In a hidden tab
matchMediachangeevents 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; usegrid-cols-[minmax(0,1fr)]. break-allsplits ordinary words mid-word ("App-s"); only use it on text without spaces.