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>
This commit is contained in:
138 files changed
+15942
-24010
No files matched your search
@@ -0,0 +1,71 @@
|
||||
# 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](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.
|
||||
Reference in new issue
Block a user