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,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseApiEvent, parseApiEvents } from "@/lib/events";
|
||||
import type { ApiEvent } from "@/lib/types";
|
||||
|
||||
const row = (overrides: Partial<ApiEvent> = {}): ApiEvent => ({
|
||||
eventId: 7,
|
||||
calendarId: 5,
|
||||
uuid: "u-7",
|
||||
name: "Anna",
|
||||
description: null,
|
||||
startDateTime: "1990-10-02T22:00:00.000Z",
|
||||
endDateTime: "1990-10-03T21:59:00.000Z",
|
||||
createdDate: "2024-01-01T10:00:00.000Z",
|
||||
lastModifiedDate: "2024-02-01T10:00:00.000Z",
|
||||
location: null,
|
||||
createdBy: null,
|
||||
lastModifiedBy: "Paula",
|
||||
url: null,
|
||||
wholeDay: 1,
|
||||
repeatFrequency: "YEARLY",
|
||||
status: "DRAFT",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("parseApiEvent", () => {
|
||||
it("coerces the raw row: 1 → true, nulls → empty strings, ISO → Date", () => {
|
||||
const event = parseApiEvent(row());
|
||||
expect(event).toMatchObject({
|
||||
eventId: 7,
|
||||
wholeDay: true,
|
||||
description: "",
|
||||
location: "",
|
||||
url: "",
|
||||
createdBy: "",
|
||||
lastModifiedBy: "Paula",
|
||||
repeatFrequency: "YEARLY",
|
||||
status: "DRAFT",
|
||||
});
|
||||
expect(event?.start.toISOString()).toBe("1990-10-02T22:00:00.000Z");
|
||||
});
|
||||
|
||||
it("drops deleted events", () => {
|
||||
expect(parseApiEvent(row({ status: "DELETED" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("drops a row without a usable start or end - what a deleted version looks like", () => {
|
||||
expect(parseApiEvent(row({ status: null, startDateTime: null }))).toBeNull();
|
||||
expect(parseApiEvent(row({ endDateTime: "not a date" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("reads a missing status as PUBLIC (the public listing omits it)", () => {
|
||||
const withoutStatus = row();
|
||||
delete withoutStatus.status;
|
||||
expect(parseApiEvent(withoutStatus)?.status).toBe("PUBLIC");
|
||||
});
|
||||
|
||||
it("never promotes an unknown status to PUBLIC", () => {
|
||||
expect(parseApiEvent(row({ status: "ARCHIVED" }))?.status).toBe("PRIVATE");
|
||||
});
|
||||
|
||||
it("reads 0 and null as not whole-day", () => {
|
||||
expect(parseApiEvent(row({ wholeDay: 0 }))?.wholeDay).toBe(false);
|
||||
expect(parseApiEvent(row({ wholeDay: null }))?.wholeDay).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseApiEvents", () => {
|
||||
it("keeps only the rows worth showing", () => {
|
||||
const events = parseApiEvents([row({ eventId: 1 }), row({ eventId: 2, status: "DELETED" }), row({ eventId: 3 })]);
|
||||
expect(events.map((e) => e.eventId)).toEqual([1, 3]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user