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,86 @@
|
||||
/**
|
||||
* Shapes on the wire and in the app.
|
||||
*
|
||||
* `ApiEvent` is what `GET /calendar/events/:calendar/json` actually returns,
|
||||
* which is looser than the API's own `Event` interface claims: the service maps
|
||||
* raw MariaDB rows straight through (`toEvent` in events.service.ts), so
|
||||
* `wholeDay` arrives as whatever the driver produced for the column (1/0 rather
|
||||
* than true/false is the likely case), and a deleted event's latest version is
|
||||
* a row with only `status` set - every other version column is null. `status`
|
||||
* is missing entirely from the anonymous public listing.
|
||||
*
|
||||
* `CalendarEvent` is the parsed, trustworthy form everything else in the app
|
||||
* works with. Parsing happens once, in `parseApiEvent`.
|
||||
*/
|
||||
|
||||
export type EventStatus = "PUBLIC" | "PRIVATE" | "DRAFT" | "DELETED";
|
||||
|
||||
export interface ApiEvent {
|
||||
eventId: number;
|
||||
calendarId: number;
|
||||
uuid: string;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
startDateTime: string | null;
|
||||
endDateTime: string | null;
|
||||
createdDate: string | null;
|
||||
lastModifiedDate?: string | null;
|
||||
location: string | null;
|
||||
createdBy?: string | null;
|
||||
lastModifiedBy?: string | null;
|
||||
url: string | null;
|
||||
wholeDay: boolean | number | null;
|
||||
repeatFrequency: string | null;
|
||||
status?: string | null;
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
eventId: number;
|
||||
calendarId: number;
|
||||
uuid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** Instants. Wall-clock meaning is always Europe/Berlin, see lib/dates.ts. */
|
||||
start: Date;
|
||||
end: Date;
|
||||
created: Date | null;
|
||||
lastModified: Date | null;
|
||||
location: string;
|
||||
createdBy: string;
|
||||
lastModifiedBy: string;
|
||||
url: string;
|
||||
wholeDay: boolean;
|
||||
/** "" when the event does not repeat. Sent to iCal verbatim as RRULE:FREQ=… */
|
||||
repeatFrequency: string;
|
||||
status: EventStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* The body of POST /calendar/events, PUT /calendar/events/:id and
|
||||
* PUT /calendar/events/move/:id.
|
||||
*
|
||||
* Every field is required here on purpose. PUT replaces the whole version and
|
||||
* defaults whatever is missing - `repeatFrequency ?? ''`, `status ?? 'PUBLIC'`
|
||||
* (events.router.ts) - so a partial body silently drops a birthday's
|
||||
* recurrence or publishes a draft.
|
||||
*/
|
||||
export interface EventPayload {
|
||||
calendarId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
startDateTime: string;
|
||||
endDateTime: string;
|
||||
location: string;
|
||||
url: string;
|
||||
wholeDay: boolean;
|
||||
repeatFrequency: string;
|
||||
status: EventStatus;
|
||||
}
|
||||
|
||||
/** GET /admin/me - the signed-in account, shared by all Nachklang apps. */
|
||||
export interface AdminUser {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
apps: string[];
|
||||
}
|
||||
Reference in new issue
Block a user