import { type FormEvent, useId, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Calendar, Pencil, Plus, Trash2 } from "lucide-react"; import { $api } from "@/client/lib/api"; import { useMe } from "@/client/auth/use-me"; import type { Calendar as CalendarRecord } from "@/client/lib/types"; import { Button } from "@/client/components/ui/button"; import { Card } from "@/client/components/ui/card"; import { ConfirmDialog } from "@/client/components/ui/confirm-dialog"; import { Copyable } from "@/client/components/ui/copyable"; import { Dialog } from "@/client/components/ui/dialog"; import { EmptyState } from "@/client/components/empty-state"; import { ErrorBanner } from "@/client/components/error-banner"; import { Input, Textarea } from "@/client/components/ui/input"; import { Label } from "@/client/components/ui/label"; import { PageHeader } from "@/client/components/ui/page-header"; import { Skeleton } from "@/client/components/loading-panel"; import { formatDate } from "@/client/lib/format"; import { toast } from "@/client/lib/toast"; const CALENDARS_KEY = ["get", "/api/v1/calendars"] as const; function trimTrailingSlash(value: string): string { return value.endsWith("/") ? value.slice(0, -1) : value; } export function CalendarsPage() { const list = $api.useQuery("get", "/api/v1/calendars", {}); const me = useMe(); const davOrigin = me.data ? trimTrailingSlash(me.data.urls.root) : ""; const [creating, setCreating] = useState(false); const [editing, setEditing] = useState(null); return (
); } function CalendarRow({ calendar, davOrigin, onEdit, }: { calendar: CalendarRecord; davOrigin: string; onEdit: () => void; }) { const queryClient = useQueryClient(); const [confirming, setConfirming] = useState(false); const remove = $api.useMutation("delete", "/api/v1/calendars/{calendar_id}", { onSuccess: () => { toast.success(`Deleted ${calendar.display_name}.`); void queryClient.invalidateQueries({ queryKey: CALENDARS_KEY }); setConfirming(false); }, }); return (
{calendar.description ?

{calendar.description}

: null}

{calendar.name}

Modified {formatDate(calendar.modified_at, { relative: true })}

DAV URL
setConfirming(false)} onConfirm={() => remove.mutate({ params: { path: { calendar_id: calendar.id } } })} title={`Delete ${calendar.display_name}?`} description="All events in this calendar will be removed. This cannot be undone." confirmLabel="Delete calendar" destructive loading={remove.isPending} >

DAV URL: {calendar.href}

); } function CalendarFormDialog({ mode, open, onClose, existing, }: { mode: "create" | "edit"; open: boolean; onClose: () => void; existing: CalendarRecord | null; }) { const queryClient = useQueryClient(); const create = $api.useMutation("post", "/api/v1/calendars", { onSuccess: () => { void queryClient.invalidateQueries({ queryKey: CALENDARS_KEY }); onClose(); }, }); const update = $api.useMutation("patch", "/api/v1/calendars/{calendar_id}", { onSuccess: () => { void queryClient.invalidateQueries({ queryKey: CALENDARS_KEY }); onClose(); }, }); const pending = mode === "create" ? create.isPending : update.isPending; const error = mode === "create" ? create.error : update.error; const formId = useId(); return ( { if (!pending) onClose(); }} title={mode === "create" ? "New calendar" : `Edit ${existing?.display_name ?? "calendar"}`} description={mode === "create" ? "Create a calendar collection." : "Update this calendar's metadata."} footer={ <> } > { if (mode === "create") { create.mutate({ body: values as Parameters[0]["body"] }); } else if (existing) { update.mutate({ params: { path: { calendar_id: existing.id } }, body: values, }); } }} error={error} /> ); } function CalendarForm({ formId, mode, existing, onSubmit, error, }: { formId: string; mode: "create" | "edit"; existing: CalendarRecord | null; onSubmit: (values: { name?: string; display_name?: string; description?: string | null; color?: string | null; order_index?: number; }) => void; error: unknown; }) { const [name, setName] = useState(existing?.name ?? ""); const [displayName, setDisplayName] = useState(existing?.display_name ?? ""); const [description, setDescription] = useState(existing?.description ?? ""); const [color, setColor] = useState(existing?.color ?? "#747178"); const submit = (event: FormEvent) => { event.preventDefault(); if (mode === "create") { onSubmit({ name: name.trim(), display_name: displayName.trim() || undefined, description: description.trim() || undefined, color: color || undefined, }); } else { onSubmit({ display_name: displayName.trim() || undefined, description: description.trim() ? description.trim() : null, color: color || null, }); } }; return (
{mode === "create" ? ( ) : null}