import { useState } from 'react'; import { Building2, Mail, Phone, User, Pencil, Trash2, Check, X } from 'lucide-react'; import { useDavContacts, useSelectedCollection } from './useDavData'; import { useDavEdit, setProperty } from './useDavEdit'; import { useTranslation } from 'officerdev'; // Right panel of /contacts: the selected address book as cards, sorted by name. // // `hasPhoto` is a flag, not the bytes — the sidecar keeps embedded photos out of the list response so a // book with a hundred cards is not a hundred base64 blobs. Until there is an endpoint that serves one // card's photo, an initial stands in. const initials = (name: string) => name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase() ?? '') .join('') || '?'; export const ContactsView = () => { const { t } = useTranslation(); const { selected, isLoading: loadingCollections } = useSelectedCollection('addressbook'); const { contacts, isLoading, error } = useDavContacts(selected?.path ?? null); const edit = useDavEdit(['caldav', 'contacts', selected?.path ?? null]); const [editing, setEditing] = useState(null); const [draft, setDraft] = useState({ fullName: '', email: '', phone: '' }); const [confirming, setConfirming] = useState(null); if (loadingCollections) return ; if (!selected) return ; if (error) return ; if (isLoading) return ; if (contacts.length === 0) return ; const sorted = [...contacts].sort((a, b) => a.fullName.localeCompare(b.fullName)); return (
{sorted.map((contact) => (
{initials(contact.fullName)}
{contact.fullName || '(no name)'}
{(contact.title || contact.org) && (
{[contact.title, contact.org].filter(Boolean).join(' · ')}
)} {contact.emails.map((email) => ( {email} ))} {contact.phones.map((phone) => ( {phone} ))} {/* Name, one email, one phone — the three fields worth correcting from a list. A card can hold several of each; editing rewrites the FIRST and leaves the rest untouched, which is why `setProperty` replaces a line rather than rebuilding the card. */} {editing === contact.href && (
{ e.preventDefault(); if (!contact.href) return; void edit .save(contact.href, (raw) => { let out = setProperty(raw, 'FN', draft.fullName); out = setProperty(out, 'EMAIL', draft.email || null); return setProperty(out, 'TEL', draft.phone || null); }) .then(() => setEditing(null)); }} > setDraft((d) => ({ ...d, fullName: e.target.value }))} placeholder="Name" className="rounded-md border bg-background px-2 py-1 text-sm" /> setDraft((d) => ({ ...d, email: e.target.value }))} placeholder="Email" className="rounded-md border bg-background px-2 py-1 text-xs" /> setDraft((d) => ({ ...d, phone: e.target.value }))} placeholder="Phone" className="rounded-md border bg-background px-2 py-1 text-xs tabular-nums" />
)}
{contact.href && editing !== contact.href && (
{confirming === contact.href ? ( <> ) : ( <> )}
)}
))}
); }; const Placeholder = ({ text }: { text: string }) => (
{text}
);