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>
87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
/**
|
|
* 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[];
|
|
}
|