"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 { ArrowLeft, Star, User, Building2, Mail, Phone, MapPin, Cake, FileText, Plus, ChevronDown, ChevronUp, X, 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 { useContactsList } from "@/lib/contacts/use-contacts-list" import { useCreateContact, useUpdateContact } from "@/lib/api/hooks/use-contact-mutations" import { fullContactToApiContact } from "@/lib/api/adapters" import { fullContactDisplayName } from "@/lib/contacts/types" import type { FullContact } from "@/lib/contacts/types" import { avatarColor, senderInitial } from "@/lib/sender-display" import { useNavStore } from "@/lib/stores/nav-store" import { cn } from "@/lib/utils" import { CONTACTS_MUTED_TEXT, CONTACTS_PAGE_AVATAR_ADD_BADGE_CLASS, CONTACTS_PAGE_AVATAR_PLACEHOLDER_LARGE_CLASS, CONTACTS_PAGE_ICON_BTN_CLASS, CONTACTS_PAGE_SAVE_BTN_CLASS, CONTACTS_PANEL_ADD_TAG_BTN_CLASS, CONTACTS_PANEL_CARD_CLASS, CONTACTS_PANEL_FLOATING_INPUT_CLASS, CONTACTS_PANEL_FLOATING_LABEL_CLASS, CONTACTS_PANEL_FLOATING_TEXTAREA_CLASS, CONTACTS_PANEL_ICON_BTN_CLASS, CONTACTS_PANEL_LINK_TEXT_CLASS, CONTACTS_PANEL_MUTED_ICON_CLASS, CONTACTS_PANEL_POPOVER_ITEM_CLASS, CONTACTS_PANEL_SELECT_TRIGGER_CLASS, CONTACTS_PANEL_TAG_CLASS, } from "@/lib/contacts-chrome-classes" 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 ContactCreatePageProps { mode: "create" | "edit" contactId?: string onBack: () => void onSaved: (id: string) => void } export function ContactCreatePage({ mode, contactId, onBack, onSaved }: ContactCreatePageProps) { const { contacts } = useContactsList() const createContactMutation = useCreateContact() const updateContactMutation = useUpdateContact() const labelRows = useNavStore((s) => s.labelRows) const availableLabels = labelRows.filter((r) => r.enabled !== false) 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 (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 tempId = crypto.randomUUID() const fullContact: FullContact = { id: tempId, ...payload, firstName: payload.firstName ?? "", lastName: payload.lastName ?? "", emails: payload.emails ?? [], phones: payload.phones ?? [], createdAt: Date.now(), updatedAt: Date.now(), } createContactMutation.mutate( { bookId: "default", contact: fullContactToApiContact(fullContact) }, { onSuccess: (created) => onSaved(created?.uid ?? tempId) }, ) onSaved(tempId) } else if (contactId) { const fullContact: FullContact = { id: contactId, ...payload, firstName: payload.firstName ?? "", lastName: payload.lastName ?? "", emails: payload.emails ?? [], phones: payload.phones ?? [], createdAt: Date.now(), updatedAt: Date.now(), } updateContactMutation.mutate({ path: contactId, contact: fullContactToApiContact(fullContact), }) onSaved(contactId) } } return (
{/* Header */}
{/* 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 */} }>
) } function FormSection({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { return (
{icon}
{children}
) } function AddButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) { return ( ) } 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) }} />
) } ) 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 (