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>
Status: agreed 2026-09-27; phases 0-8 done 2026-09-28, not yet committed or deployed. Branch: `feature/nextjs-rewrite`.
Goal: replace the Angular 18 app with a Next.js app that matches `nachklang-feedback` in stack,
look and auth behaviour, works properly on phones, and fixes the bugs found in the current
implementation instead of porting them.
---
## 1. Decisions
| # | Topic | Decision |
|---|-------|----------|
| Q1 | Repo | Rewrite in place in `Calendar_Frontend` on `feature/nextjs-rewrite`; the Angular tree is replaced, history keeps it. |
| Q2 | Hosting | `output: "standalone"` Node app on Plesk/Passenger, same as feedback. |
| Q3 | Stack | Copy feedback exactly: Next 16, React 19, Tailwind 4, shadcn `base-luma` preset `b7ClO5que`, Inter, lucide, ESLint. Port `lib/env.ts`, `lib/admin-auth.ts`, the `adminApi` request/error pattern, `proxy.ts`, and the admin layout's 401/403/unavailable handling. |
| Q4 | Language | German only, no i18n. |
| Q5 | API scope | Frontend-only, **except** the small API PR in §5 (Q21/Q22). Everything else goes to `docs/api-wishlist.md`. |
| Q6 | Admin layout | Agenda list grouped by month; editing happens in a Sheet (bottom drawer on mobile, side panel on desktop). Month grid is a later feature. |
| Q7 | Calendars | All calendars at once, colour-coded, toggled with filter chips. Filter state lives in the URL (`?cal=public,choir&range=future&q=…`). Birthdays off by default. |
| Q8 | Move | No separate "Move" action: calendar is a form field; the app calls `PUT /move/:id` when it changed. |
| Q9 | Landing page | Port the current page, add a "Nächste Termine" list from the anonymous public JSON, read the API URL from env. |
| Q10 | Tests | Vitest for pure logic (dates/timezone, whole-day, URL normalisation, validation, filter/sort, calendar mapping). No e2e. |
| Q11 | API | Read; findings in §3. |
| Q12 | Form | Fields and rules in §4.3. |
| Q13 | Libraries | Plain `useState` + pure `validateEvent()` + a small `useEvents` hook. No react-hook-form/zod/TanStack Query. |
| Q14 | Recurrence | Not editable. Birthdays get `YEARLY` automatically (moved from `api.service.ts` into form logic), shown as a "jährlich" badge. |
| Q15 | Save/delete | Sheet stays open until the API confirms; failure keeps the edits and shows the error; 401 → login. Dirty-close confirmation. Delete via AlertDialog, no undo. Quick "Veröffentlichen" on draft list items. |
| Q16 | Extras | All four: duplicate, text search, mobile "+" FAB, iCal-link copy for internal calendars (via the Q22 endpoint). |
| Q17 | Cutover | Verify locally on port 4200; the user switches the Plesk subdomain to a Node app and deploys. Old static build kept for ~1 week as rollback. |
| Q18 | Docs | Create `docs/architecture.md`, `docs/features.md`, `docs/api-wishlist.md`, `docs/gotchas.md`; update `CLAUDE.md`; add Next `AGENTS.md` note like feedback. |
| Q19 | Timezone | All form date handling explicitly in `Europe/Berlin` via `@date-fns/tz`; send UTC ISO strings. |
| Q20 | Whole-day | Date-only inputs, inclusive end date; saved as start 00:00 / end 23:59 Berlin. No bulk migration of old rows. |
| Q21 | Descriptions | Textarea stays; API PR adds RFC 5545 escaping + line folding to the iCal generator first. |
| Q22 | Internal iCal links | New API endpoint `GET /calendar/events/subscriptions` (editors only) returning the subscribe URLs. |
| Q23 | Recurring in filters | "Has a `repeatFrequency`" (not "calendar 5") means always current; sorted by next occurrence. |
| Q24 | API sequencing | One API PR on its own branch, reviewed and deployed before the frontend goes live. |
---
## 2. Problems in the Angular app this rewrite fixes
Found by reading the code on 2026-09-27; do not port these.
- 14-column table with editing inside the rows; unusable on phones. Toolbar is four `<select>`s joined by ` | `.
- All feedback via `window.alert` / `window.confirm`.
- Admin UI in English, landing page German.
- Calendar column shows the raw id ("5").
- One calendar at a time, no search.
- **Move dialog:** changes `data.event.calendarId` while you choose, so Cancel still leaves the changed id on the row, and the next Save sends it. The dropdown also has no Birthdays option.
- Calendar ids hardcoded three times (`getCalendarId`, the index-based `calendars` array in the move popup, `calendarId === 5` in `api.service.ts` and the filter).
-`Date` objects mutated in place, everything in browser local time; whole-day events keep arbitrary times.
- Save closes the edit row before the request finishes, so a failed save looks saved.
- Changing the filter re-fetches from the server.
- URL regex forces `https://` and rewrites the value as a side effect.
- Dead `LoginComponent`; landing page hardcodes `api.nachklang.art`.
---
## 3. API facts the frontend must respect
Read from `API/src/models/calendar/events/*` on 2026-09-27.
- Endpoints under `/calendar/events/`: `GET /:calendar/json` (editors get all statuses incl. `DELETED`,
anonymous callers get `PUBLIC` only; `public` needs no credential), `POST /`, `PUT /:id`,
`PUT /move/:id`, `DELETE /:id`. Writes need `requireAppAccess('calendar')`.
- Calendar ids: public 1, members 2, management 3, choir 4, birthdays 5
(`calendarNames` in `events.router.ts`). The frontend keeps these in one typed constant.
- **PUT replaces the whole event** and defaults missing fields (`repeatFrequency ?? ''`,
`status ?? 'PUBLIC'`). Always send the full event, including for quick-publish.
- PUT answering 200 with `"0 rows were updated"` counts as a failure.
- Every write inserts into `event_versions`; delete inserts a version with only `status='DELETED'`.
The admin feed therefore returns deleted rows; the frontend filters them out.
-`PRIVATE` exists in the enum but nothing uses it: show read-only as "Privat", keep it on save.
- Admin feed ignores `includes_calendars`; public feeds follow it.
- iCal: timed events are written as `TZID=Europe/Berlin` using the server's local `getHours()`,
so the API process must run in Berlin time (inferred from the code, not checked on the server).
Whole-day end date gets +1 day, i.e. the stored end is the inclusive last day.
`repeatFrequency` goes out verbatim as `RRULE:FREQ=…`.
- Internal iCal feeds need `?password=`; passwords are env vars (`MEMBER_CREDENTIAL`,
`CHOIR_CREDENTIAL` for choir **and** birthdays, `MANAGEMENT_CREDENTIAL`).
- CORS already allows `https://calendar.nachklang.art` (`app.factory.ts`).
- in-memory, seeded by the API's own dev scripts, incl. the `dev@nachklang.art` account.
- API: `npx tsc` in the API repo, then `node API/dist/app.js` started **from a scratch directory**
holding its own `.env` (dotenv reads the cwd), with `TZ=Europe/Berlin`. The API repo's own `.env`
is never loaded. The API's logger still writes to `API/logs/` (gitignored).
- Sign in by POSTing the seeded credentials to `http://localhost:3000/admin/auth/sign-in/email`
from a page on `localhost:4200`; the admin app isn't needed.
---
## 7. Open risks
- Plesk subdomain conversion from static to Node is unverified for this domain. (Corrected: an
earlier version said feedback proves the toolkit works on the server - feedback's own plan records
its Plesk deploy as blocked and never documents how it runs, so that was an unsourced claim.)
- The API server's timezone is inferred, not checked. If it isn't Berlin, the iCal times are already
wrong today, independent of this rewrite.
-`lastModifiedBy`/`createdBy` names can come back empty for legacy rows (nullable since migration 003);
the footer has to handle that.
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.