"use client" import { useState, useEffect, useId, forwardRef, useCallback, useRef, type InputHTMLAttributes, } from "react" import { useForm, useFieldArray, Controller } from "react-hook-form" import { z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" import { Star, X, User, Building2, Mail, Phone, MapPin, Cake, FileText, Plus, ChevronDown, ChevronUp, Check, } from "lucide-react" import { Button } from "@/components/ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { useContactsStore } from "@/lib/contacts/contacts-store" import { fullContactDisplayName } from "@/lib/contacts/types" import { avatarColor, senderInitial } from "@/lib/sender-display" import { useNavStore } from "@/lib/stores/nav-store" import { CONTACTS_PANEL_ADD_TAG_BTN_CLASS, CONTACTS_PANEL_AVATAR_PLACEHOLDER_CLASS, CONTACTS_PANEL_CARD_CLASS, CONTACTS_PANEL_FLOATING_INPUT_CLASS, CONTACTS_PANEL_FLOATING_LABEL_CLASS, CONTACTS_PANEL_FLOATING_TEXTAREA_CLASS, CONTACTS_PANEL_HEADER_COMPACT_CLASS, CONTACTS_PANEL_ICON_BTN_CLASS, CONTACTS_PANEL_LINK_TEXT_CLASS, CONTACTS_PANEL_MUTED_ICON_CLASS, CONTACTS_PANEL_POPOVER_ITEM_CLASS, CONTACTS_PANEL_SAVE_BTN_CLASS, CONTACTS_PANEL_SELECT_TRIGGER_CLASS, CONTACTS_PANEL_SHELL_CLASS, CONTACTS_PANEL_TAG_CLASS, CONTACTS_MUTED_TEXT, } from "@/lib/contacts-chrome-classes" import { cn } from "@/lib/utils" import { ContactsPanelLogo } from "./contacts-panel-logo" const FRENCH_MONTHS = [ "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre", ] as const const EMAIL_LABELS = ["Domicile", "Travail", "Autre"] as const const PHONE_LABELS = ["Mobile", "Domicile", "Travail"] as const const ADDRESS_LABELS = ["Domicile", "Travail", "Autre"] as const const addressSchema = z.object({ street: z.string().optional().default(""), city: z.string().optional().default(""), region: z.string().optional().default(""), postalCode: z.string().optional().default(""), country: z.string().optional().default(""), label: z.string().default("Domicile"), }) const contactFormSchema = z.object({ namePrefix: z.string().optional().default(""), firstName: z.string().optional().default(""), middleName: z.string().optional().default(""), lastName: z.string().optional().default(""), nameSuffix: z.string().optional().default(""), phoneticFirstName: z.string().optional().default(""), phoneticLastName: z.string().optional().default(""), company: z.string().optional().default(""), department: z.string().optional().default(""), jobTitle: z.string().optional().default(""), emails: z.array( z.object({ value: z.string(), label: z.string(), }), ), phones: z.array( z.object({ value: z.string(), label: z.string(), }), ), addresses: z.array(addressSchema), birthday: z .object({ day: z.any().optional(), month: z.any().optional(), year: z.any().optional(), }) .optional(), notes: z.string().optional().default(""), labels: z.array(z.string()).optional().default([]), }) type ContactFormValues = z.infer interface ContactFormViewProps { mode: "create" | "edit" contactId?: string | null } export function ContactFormView({ mode, contactId }: ContactFormViewProps) { const { contacts, addContact, updateContact, setView, showContactsList, closePanel, createDraft, clearCreateDraft, } = useContactsStore() const labelRows = useNavStore((s) => s.labelRows) const [starred, setStarred] = useState(false) const [nameExpanded, setNameExpanded] = useState(false) const [companyExpanded, setCompanyExpanded] = useState(false) const existingContact = mode === "edit" ? contacts.find((c) => c.id === contactId) : null const { register, handleSubmit, control, watch, reset, setValue, formState: { isDirty }, } = useForm({ resolver: zodResolver(contactFormSchema), defaultValues: { namePrefix: "", firstName: "", middleName: "", lastName: "", nameSuffix: "", phoneticFirstName: "", phoneticLastName: "", company: "", department: "", jobTitle: "", emails: [{ value: "", label: "Domicile" }], phones: [{ value: "", label: "Mobile" }], addresses: [], birthday: { day: undefined, month: undefined, year: undefined }, notes: "", labels: [], }, }) const { fields: emailFields, append: appendEmail, remove: removeEmail, } = useFieldArray({ control, name: "emails" }) const { fields: phoneFields, append: appendPhone, remove: removePhone, } = useFieldArray({ control, name: "phones" }) const { fields: addressFields, append: appendAddress, remove: removeAddress, } = useFieldArray({ control, name: "addresses" }) useEffect(() => { if (mode !== "create" || !createDraft) return reset({ namePrefix: "", firstName: createDraft.firstName ?? "", middleName: "", lastName: createDraft.lastName ?? "", nameSuffix: "", phoneticFirstName: "", phoneticLastName: "", company: "", department: "", jobTitle: "", emails: createDraft.emails?.length ? createDraft.emails : [{ value: "", label: "Domicile" }], phones: [{ value: "", label: "Mobile" }], addresses: [], birthday: { day: undefined, month: undefined, year: undefined }, notes: "", labels: [], }, { shouldDirty: true }) clearCreateDraft() }, [mode, createDraft, reset, clearCreateDraft]) useEffect(() => { if (existingContact) { const hasExtendedName = !!( existingContact.namePrefix || existingContact.middleName || existingContact.nameSuffix || existingContact.phoneticFirstName || existingContact.phoneticLastName ) if (hasExtendedName) setNameExpanded(true) if (existingContact.department) setCompanyExpanded(true) reset({ namePrefix: existingContact.namePrefix ?? "", firstName: existingContact.firstName, middleName: existingContact.middleName ?? "", lastName: existingContact.lastName, nameSuffix: existingContact.nameSuffix ?? "", phoneticFirstName: existingContact.phoneticFirstName ?? "", phoneticLastName: existingContact.phoneticLastName ?? "", company: existingContact.company ?? "", department: existingContact.department ?? "", jobTitle: existingContact.jobTitle ?? "", emails: existingContact.emails.length ? existingContact.emails : [{ value: "", label: "Domicile" }], phones: existingContact.phones.length ? existingContact.phones : [{ value: "", label: "Mobile" }], addresses: existingContact.addresses ?? [], birthday: existingContact.birthday ?? { day: undefined, month: undefined, year: undefined, }, notes: existingContact.notes ?? "", labels: existingContact.labels ?? [], }) } }, [existingContact, reset]) const firstName = watch("firstName") const lastName = watch("lastName") const watchedEmails = watch("emails") const currentLabels = watch("labels") ?? [] const displayName = `${firstName ?? ""} ${lastName ?? ""}`.trim() const canSave = isDirty || (mode === "create" && !!( firstName?.trim() || lastName?.trim() || watchedEmails?.some((e) => e.value?.trim()) )) const toggleLabel = useCallback( (labelId: string) => { const next = currentLabels.includes(labelId) ? currentLabels.filter((l) => l !== labelId) : [...currentLabels, labelId] setValue("labels", next, { shouldDirty: true }) }, [currentLabels, setValue], ) function onSubmit(data: ContactFormValues) { const payload = { namePrefix: data.namePrefix || undefined, firstName: data.firstName ?? "", middleName: data.middleName || undefined, lastName: data.lastName ?? "", nameSuffix: data.nameSuffix || undefined, phoneticFirstName: data.phoneticFirstName || undefined, phoneticLastName: data.phoneticLastName || undefined, company: data.company || undefined, department: data.department || undefined, jobTitle: data.jobTitle || undefined, emails: data.emails.filter((e) => e.value), phones: data.phones.filter((p) => p.value), addresses: data.addresses.filter( (a) => a.street || a.city || a.region || a.postalCode || a.country, ), birthday: data.birthday?.day || data.birthday?.month || data.birthday?.year ? data.birthday : undefined, notes: data.notes || undefined, labels: data.labels?.length ? data.labels : undefined, } if (mode === "create") { const id = addContact(payload) setView("view", id) } else if (contactId) { updateContact(contactId, payload) setView("view", contactId) } } const availableLabels = labelRows.filter((r) => r.enabled !== false) return (
{/* Avatar */}
{displayName ? (
{senderInitial(displayName)}
) : (
)}
{/* Labels */}
{currentLabels.map((labelId) => { const row = labelRows.find((r) => r.id === labelId) return ( {row && ( )} {row?.label ?? labelId} ) })}

Libellés

{availableLabels.map((row) => { const active = currentLabels.includes(row.id) return ( ) })}
{/* Name section */} }> {nameExpanded && ( )}
{nameExpanded && ( )} {nameExpanded && ( <> )}
{/* Company section */} }>
{companyExpanded && ( )}
{/* Email section */} }> {emailFields.map((field, index) => (
{emailFields.length > 1 && ( )}
( ({ value: l, label: l }))} /> )} />
))} appendEmail({ value: "", label: "Domicile" })}> Ajouter une adresse e-mail
{/* Phone section */} }> {phoneFields.map((field, index) => (
🇫🇷
{phoneFields.length > 1 && ( )}
( ({ value: l, label: l }))} /> )} />
))} appendPhone({ value: "", label: "Mobile" })}> Ajouter un numéro de téléphone
{/* Address section */} }> {addressFields.map((field, index) => (
( ({ value: l, label: l }))} /> )} />
))} appendAddress({ street: "", city: "", region: "", postalCode: "", country: "", label: "Domicile", }) } > Ajouter une adresse
{/* Birthday section */} }>
( f.onChange(v ? Number(v) : undefined)} options={FRENCH_MONTHS.map((name, i) => ({ value: String(i + 1), label: name, }))} placeholder="Mois" /> )} />
{/* Notes section */} }>
) } /* ─── Layout helpers ─────────────────────────────────────────── */ function FormSection({ icon, children, }: { icon: React.ReactNode children: React.ReactNode }) { return (
{icon}
{children}
) } function AddButton({ onClick, children, }: { onClick: () => void children: React.ReactNode }) { return ( ) } /* ─── Floating label input (Material outlined style) ─────── */ interface FloatingInputProps extends InputHTMLAttributes { label: string } const FloatingInput = forwardRef( function FloatingInput({ label, className, defaultValue, ...props }, ref) { const id = useId() const [focused, setFocused] = useState(false) const [filled, setFilled] = useState(() => !!defaultValue) const innerRef = useRef(null) useEffect(() => { if (innerRef.current && innerRef.current.value) setFilled(true) }) const setRefs = useCallback( (node: HTMLInputElement | null) => { innerRef.current = node if (typeof ref === "function") ref(node) else if (ref) ref.current = node if (node && node.value) setFilled(true) }, [ref], ) const floated = focused || filled return (
{ setFocused(true) props.onFocus?.(e) }} onBlur={(e) => { setFocused(false) setFilled(!!e.target.value) props.onBlur?.(e) }} onChange={(e) => { setFilled(!!e.target.value) props.onChange?.(e) }} />
) }, ) /* ─── Floating label textarea ────────────────────────────── */ interface FloatingTextareaProps extends React.TextareaHTMLAttributes { label: string } const FloatingTextarea = forwardRef( function FloatingTextarea({ label, className, ...props }, ref) { const id = useId() const [focused, setFocused] = useState(false) const [filled, setFilled] = useState(false) const innerRef = useRef(null) useEffect(() => { if (innerRef.current && innerRef.current.value) setFilled(true) }) const setRefs = useCallback( (node: HTMLTextAreaElement | null) => { innerRef.current = node if (typeof ref === "function") ref(node) else if (ref) ref.current = node if (node && node.value) setFilled(true) }, [ref], ) const floated = focused || filled return (