"use client"; import { CalendarPlusIcon, CheckIcon, CircleAlertIcon, CopyIcon, RefreshCwIcon } from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { goToAdminLogin } from "@/lib/admin-auth"; import { api } from "@/lib/api"; import { describeApiError, isSessionExpired } from "@/lib/api-errors"; import { CALENDARS, type CalendarSlug } from "@/lib/calendars"; import { toGoogleCalendar, toWebcal } from "@/lib/public-calendar"; import { cn } from "@/lib/utils"; type Subscription = { calendar: CalendarSlug; icalUrl: string }; type Load = { status: "idle" } | { status: "loading" } | { status: "ready"; subscriptions: Subscription[] } | { status: "error"; message: string }; /** * Subscription links for every calendar, for the editor's own phone or to * pass on to members. The URLs of the internal calendars carry their shared * password, which is why they come from the API (GET * /calendar/events/subscriptions, editors only) rather than being built here - * the browser has no other way to know the passwords. * * Fetched each time the dialog opens, so a rotated password shows up at once. */ export function SubscriptionsDialog() { const [load, setLoad] = useState({ status: "idle" }); const [copied, setCopied] = useState(null); const fetchLinks = async () => { setLoad({ status: "loading" }); try { setLoad({ status: "ready", subscriptions: await api.getSubscriptions() }); } catch (e) { if (isSessionExpired(e)) { goToAdminLogin(); return; } setLoad({ status: "error", message: describeApiError(e) }); } }; const copy = async (subscription: Subscription) => { try { await navigator.clipboard.writeText(subscription.icalUrl); setCopied(subscription.calendar); setTimeout(() => setCopied((current) => (current === subscription.calendar ? null : current)), 2000); } catch { // No clipboard access: the URL is selectable in the dialog instead. setCopied(null); } }; return ( { if (open) void fetchLinks(); }} > }> Abonnieren {/* minmax(0,1fr): an implicit grid column grows to its longest unbreakable content - here a full URL - so `truncate` on the URL never kicked in and the dialog's content overflowed. */} Kalender abonnieren Mit diesen Links erscheinen die Termine automatisch in einer Kalender-App. Die Links der internen Kalender enthalten das jeweilige Kalender-Passwort – bitte nur an Mitglieder weitergeben. {load.status === "loading" || load.status === "idle" ? (
{CALENDARS.map((c) => ( ))}
) : load.status === "error" ? (

Die Links konnten nicht geladen werden: {load.message}

) : (
    {CALENDARS.map((calendar) => { const subscription = load.subscriptions.find((s) => s.calendar === calendar.slug); return (
  • {calendar.label} {subscription ? ( <> {subscription.icalUrl}
    ) : (

    Für diesen Kalender ist auf dem Server kein Passwort eingerichtet, daher gibt es keinen Abo-Link.

    )}
  • ); })}
)}

Chor und Geburtstage teilen sich ein Passwort: Wird es geändert, müssen beide neu abonniert werden.

); }