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,82 @@
|
||||
/**
|
||||
* The calendars, in one place.
|
||||
*
|
||||
* The Angular app kept this mapping three times - a switch in the admin page,
|
||||
* an index-into-array in the move popup (which only worked because ids 3 and 4
|
||||
* happen to sit in that order) and `calendarId === 5` checks in the API service
|
||||
* and the filter - and the copies had drifted: the move popup could not move an
|
||||
* event to Birthdays.
|
||||
*
|
||||
* Ids and slugs mirror `calendarNames` in the API's events.router.ts; the API
|
||||
* has no endpoint that lists them (docs/api-wishlist.md), so a new calendar
|
||||
* needs an entry here too.
|
||||
*
|
||||
* Class names are written out in full so Tailwind's source scan finds them.
|
||||
*/
|
||||
|
||||
export type CalendarSlug = "public" | "members" | "choir" | "management" | "birthdays";
|
||||
|
||||
export interface Calendar {
|
||||
id: number;
|
||||
slug: CalendarSlug;
|
||||
label: string;
|
||||
/** Shown by default in the admin list. Birthdays would flood it. */
|
||||
defaultVisible: boolean;
|
||||
/**
|
||||
* A recurrence every event in this calendar gets, whatever the form says.
|
||||
* Replaces the old `calendarId === 5` special case in api.service.ts.
|
||||
*/
|
||||
forcedRepeatFrequency?: string;
|
||||
dotClass: string;
|
||||
badgeClass: string;
|
||||
}
|
||||
|
||||
export const CALENDARS: readonly Calendar[] = [
|
||||
{
|
||||
id: 1,
|
||||
slug: "public",
|
||||
label: "Öffentlich",
|
||||
defaultVisible: true,
|
||||
dotClass: "bg-sky-500",
|
||||
badgeClass: "bg-sky-50 text-sky-800 ring-sky-200",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
slug: "members",
|
||||
label: "Mitglieder",
|
||||
defaultVisible: true,
|
||||
dotClass: "bg-emerald-500",
|
||||
badgeClass: "bg-emerald-50 text-emerald-800 ring-emerald-200",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
slug: "choir",
|
||||
label: "Chor",
|
||||
defaultVisible: true,
|
||||
dotClass: "bg-violet-500",
|
||||
badgeClass: "bg-violet-50 text-violet-800 ring-violet-200",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
slug: "management",
|
||||
label: "Management",
|
||||
defaultVisible: true,
|
||||
dotClass: "bg-amber-500",
|
||||
badgeClass: "bg-amber-50 text-amber-900 ring-amber-200",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
slug: "birthdays",
|
||||
label: "Geburtstage",
|
||||
defaultVisible: false,
|
||||
forcedRepeatFrequency: "YEARLY",
|
||||
dotClass: "bg-rose-500",
|
||||
badgeClass: "bg-rose-50 text-rose-800 ring-rose-200",
|
||||
},
|
||||
];
|
||||
|
||||
export const calendarById = (id: number): Calendar | undefined => CALENDARS.find((c) => c.id === id);
|
||||
|
||||
export const calendarBySlug = (slug: string): Calendar | undefined => CALENDARS.find((c) => c.slug === slug);
|
||||
|
||||
export const DEFAULT_VISIBLE_CALENDAR_IDS: readonly number[] = CALENDARS.filter((c) => c.defaultVisible).map((c) => c.id);
|
||||
Reference in new issue
Block a user