import { type FormEvent, useId, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { BookUser, Pencil, Plus, Trash2 } from "lucide-react"; import { $api } from "@/client/lib/api"; import { useMe } from "@/client/auth/use-me"; import type { Addressbook } 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 ADDRESSBOOKS_KEY = ["get", "/api/v1/addressbooks"] as const; function trimTrailingSlash(value: string): string { return value.endsWith("/") ? value.slice(0, -1) : value; } export function AddressbooksPage() { const list = $api.useQuery("get", "/api/v1/addressbooks", {}); const me = useMe(); const davOrigin = me.data ? trimTrailingSlash(me.data.urls.root) : ""; const [creating, setCreating] = useState(false); const [editing, setEditing] = useState(null); return (
); } function AddressbookRow({ book, davOrigin, onEdit }: { book: Addressbook; davOrigin: string; onEdit: () => void }) { const queryClient = useQueryClient(); const [confirming, setConfirming] = useState(false); const remove = $api.useMutation("delete", "/api/v1/addressbooks/{addressbook_id}", { onSuccess: () => { toast.success(`Deleted ${book.display_name}.`); void queryClient.invalidateQueries({ queryKey: ADDRESSBOOKS_KEY }); setConfirming(false); }, }); return (

{book.display_name}

{book.description ?

{book.description}

: null}

{book.name}

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

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

DAV URL: {book.href}

); } function AddressbookFormDialog({ mode, open, onClose, existing, }: { mode: "create" | "edit"; open: boolean; onClose: () => void; existing: Addressbook | null; }) { const queryClient = useQueryClient(); const create = $api.useMutation("post", "/api/v1/addressbooks", { onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ADDRESSBOOKS_KEY }); onClose(); }, }); const update = $api.useMutation("patch", "/api/v1/addressbooks/{addressbook_id}", { onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ADDRESSBOOKS_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 address book" : `Edit ${existing?.display_name ?? "address book"}`} description={mode === "create" ? "Create a contact collection." : "Update this address book."} footer={ <> } > { if (mode === "create") { create.mutate({ body: { name: values.name?.trim() ?? "", display_name: values.display_name, description: values.description ?? undefined, }, }); } else if (existing) { update.mutate({ params: { path: { addressbook_id: existing.id } }, body: { display_name: values.display_name, description: values.description, }, }); } }} error={error} /> ); } function AddressbookForm({ formId, mode, existing, onSubmit, error, }: { formId: string; mode: "create" | "edit"; existing: Addressbook | null; onSubmit: (values: { name?: string; display_name?: string; description?: string | null }) => void; error: unknown; }) { const [name, setName] = useState(existing?.name ?? ""); const [displayName, setDisplayName] = useState(existing?.display_name ?? ""); const [description, setDescription] = useState(existing?.description ?? ""); const submit = (event: FormEvent) => { event.preventDefault(); if (mode === "create") { onSubmit({ name: name.trim(), display_name: displayName.trim() || undefined, description: description.trim() || undefined, }); } else { onSubmit({ display_name: displayName.trim() || undefined, description: description.trim() ? description.trim() : null, }); } }; return (
{mode === "create" ? ( ) : null}