Files
Calendar_Frontend/components/admin/PublishDialog.tsx
T
PaddyandClaude Opus 5.5 0cda40f85e 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>
2026-09-28 22:15:59 +02:00

80 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { LoaderCircleIcon, SendIcon } from "lucide-react";
import { CalendarBadge } from "@/components/admin/CalendarBadge";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { calendarById } from "@/lib/calendars";
import { formatDay } from "@/lib/dates";
import { describeWhen, occurrence } from "@/lib/event-display";
import type { CalendarEvent } from "@/lib/types";
/**
* "Really publish?" - publishing puts the event into every subscriber's
* calendar app, and for the public calendar onto nachklang.art, which is not
* something to do by a stray tap on a phone.
*
* Stays open with a spinner until the API has answered, and stays open on a
* failure (the page shows the error as a toast), so a retry is one tap.
*/
export function PublishDialog({
event,
now,
publishing,
onConfirm,
onCancel,
}: {
event: CalendarEvent | null;
now: Date;
publishing: boolean;
onConfirm: (event: CalendarEvent) => void;
onCancel: () => void;
}) {
const calendar = event ? calendarById(event.calendarId) : undefined;
return (
<AlertDialog
open={event !== null}
onOpenChange={(open) => {
if (!open && !publishing) onCancel();
}}
>
<AlertDialogContent>
{event && (
<>
<AlertDialogHeader>
<AlertDialogTitle>„{event.name}“ veröffentlichen?</AlertDialogTitle>
<AlertDialogDescription>
{calendar?.slug === "public"
? "Der Termin wird danach öffentlich angezeigt – auf nachklang.art und bei allen, die den Kalender abonniert haben."
: `Der Termin erscheint danach bei allen, die den Kalender „${calendar?.label ?? event.calendarId}“ abonniert haben.`}
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex flex-col gap-1.5 rounded-lg bg-muted/60 px-3 py-2.5 text-sm">
<span>
{formatDay(occurrence(event, now).start)} · {describeWhen(event, now)}
</span>
<CalendarBadge calendarId={event.calendarId} className="self-start" />
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={publishing}>Abbrechen</AlertDialogCancel>
<Button onClick={() => onConfirm(event)} disabled={publishing}>
{publishing ? <LoaderCircleIcon className="animate-spin" /> : <SendIcon />}
Veröffentlichen
</Button>
</AlertDialogFooter>
</>
)}
</AlertDialogContent>
</AlertDialog>
);
}