248 lines
8.5 KiB
TypeScript
248 lines
8.5 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
ArrowLeft,
|
|
Download,
|
|
Pencil,
|
|
Star,
|
|
Trash2,
|
|
Mail,
|
|
Phone,
|
|
Building2,
|
|
MapPin,
|
|
Cake,
|
|
FileText,
|
|
MessageSquare,
|
|
Video,
|
|
} from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
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 { downloadContactVCard } from "@/lib/contacts/export-contacts"
|
|
import {
|
|
CONTACTS_HEADING_TEXT,
|
|
CONTACTS_MUTED_TEXT,
|
|
CONTACTS_PAGE_ICON_BTN_CLASS,
|
|
CONTACTS_PAGE_TAG_CLASS,
|
|
CONTACTS_PANEL_DIVIDER_CLASS,
|
|
CONTACTS_PANEL_MUTED_ICON_CLASS,
|
|
CONTACTS_PANEL_PRIMARY_ACTION_CLASS,
|
|
CONTACTS_PANEL_SECONDARY_ICON_BTN_CLASS,
|
|
} from "@/lib/contacts-chrome-classes"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const FRENCH_MONTHS = [
|
|
"janvier", "février", "mars", "avril", "mai", "juin",
|
|
"juillet", "août", "septembre", "octobre", "novembre", "décembre",
|
|
]
|
|
|
|
function formatBirthday(b: { day?: number; month?: number; year?: number }): string {
|
|
const parts: string[] = []
|
|
if (b.day) parts.push(String(b.day))
|
|
if (b.month) parts.push(FRENCH_MONTHS[b.month - 1] ?? "")
|
|
if (b.year) parts.push(String(b.year))
|
|
return parts.join(" ")
|
|
}
|
|
|
|
interface ContactDetailPageProps {
|
|
contactId: string
|
|
onBack: () => void
|
|
onEdit: (id: string) => void
|
|
}
|
|
|
|
export function ContactDetailPage({ contactId, onBack, onEdit }: ContactDetailPageProps) {
|
|
const { contacts, softDeleteContact } = useContactsStore()
|
|
const labelRows = useNavStore((s) => s.labelRows)
|
|
const contact = contacts.find((c) => c.id === contactId)
|
|
|
|
if (!contact) {
|
|
return (
|
|
<div className={cn("flex h-full items-center justify-center text-sm", CONTACTS_MUTED_TEXT)}>
|
|
Contact introuvable
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const displayName = fullContactDisplayName(contact)
|
|
const name = displayName || contact.emails[0]?.value || contact.phones[0]?.value || "?"
|
|
const color = avatarColor(name)
|
|
const initial = senderInitial(name)
|
|
const primaryEmail = contact.emails[0]?.value
|
|
|
|
function handleDelete() {
|
|
softDeleteContact(contactId, "Supprimé manuellement")
|
|
onBack()
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto max-w-3xl px-6 py-8 text-foreground">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={CONTACTS_PAGE_ICON_BTN_CLASS}
|
|
onClick={onBack}
|
|
>
|
|
<ArrowLeft className="h-5 w-5" />
|
|
</Button>
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="icon" className={CONTACTS_PAGE_ICON_BTN_CLASS}>
|
|
<Star className="h-5 w-5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={CONTACTS_PAGE_ICON_BTN_CLASS}
|
|
onClick={() => downloadContactVCard(contact)}
|
|
aria-label="Télécharger la fiche contact"
|
|
>
|
|
<Download className="h-5 w-5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={CONTACTS_PAGE_ICON_BTN_CLASS}
|
|
onClick={() => onEdit(contactId)}
|
|
>
|
|
<Pencil className="h-5 w-5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={CONTACTS_PAGE_ICON_BTN_CLASS}
|
|
onClick={handleDelete}
|
|
>
|
|
<Trash2 className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-6 pb-6">
|
|
{contact.avatarUrl ? (
|
|
<img src={contact.avatarUrl} alt={name} className="h-24 w-24 rounded-full object-cover" />
|
|
) : (
|
|
<div
|
|
className="flex h-24 w-24 items-center justify-center rounded-full text-3xl font-medium text-white"
|
|
style={{ backgroundColor: color }}
|
|
>
|
|
{initial}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<h1 className={cn("text-3xl", CONTACTS_HEADING_TEXT)}>{name}</h1>
|
|
{contact.company && (
|
|
<p className={cn("mt-1 text-base", CONTACTS_MUTED_TEXT)}>
|
|
{contact.jobTitle ? `${contact.jobTitle} — ` : ""}
|
|
{contact.company}
|
|
</p>
|
|
)}
|
|
{contact.labels && contact.labels.length > 0 && (
|
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
{contact.labels.map((labelId) => {
|
|
const row = labelRows.find((r) => r.id === labelId)
|
|
return row ? (
|
|
<span key={labelId} className={CONTACTS_PAGE_TAG_CLASS}>
|
|
<span className={`inline-block h-2 w-2 rounded-full ${row.color}`} />
|
|
{row.label}
|
|
</span>
|
|
) : null
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{primaryEmail && (
|
|
<div className={cn("flex items-center gap-2 py-4", CONTACTS_PANEL_DIVIDER_CLASS)}>
|
|
<button type="button" className={CONTACTS_PANEL_PRIMARY_ACTION_CLASS}>
|
|
<Mail className="h-4 w-4" />
|
|
Envoyer un e-mail
|
|
</button>
|
|
<button type="button" className={CONTACTS_PANEL_SECONDARY_ICON_BTN_CLASS}>
|
|
<MessageSquare className="h-4 w-4" />
|
|
</button>
|
|
<button type="button" className={CONTACTS_PANEL_SECONDARY_ICON_BTN_CLASS}>
|
|
<Video className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className={cn("space-y-1 pt-4", CONTACTS_PANEL_DIVIDER_CLASS)}>
|
|
{contact.emails.length > 0 && (
|
|
<DetailRow icon={<Mail className={cn("h-5 w-5", CONTACTS_PANEL_MUTED_ICON_CLASS)} />}>
|
|
{contact.emails.map((e, i) => (
|
|
<div key={i}>
|
|
<p className="text-sm text-primary">{e.value}</p>
|
|
<p className={cn("text-xs", CONTACTS_MUTED_TEXT)}>{e.label}</p>
|
|
</div>
|
|
))}
|
|
</DetailRow>
|
|
)}
|
|
|
|
{contact.phones.length > 0 && (
|
|
<DetailRow icon={<Phone className={cn("h-5 w-5", CONTACTS_PANEL_MUTED_ICON_CLASS)} />}>
|
|
{contact.phones.map((p, i) => (
|
|
<div key={i}>
|
|
<p className="text-sm text-primary">{p.value}</p>
|
|
<p className={cn("text-xs", CONTACTS_MUTED_TEXT)}>{p.label}</p>
|
|
</div>
|
|
))}
|
|
</DetailRow>
|
|
)}
|
|
|
|
{contact.company && (
|
|
<DetailRow icon={<Building2 className={cn("h-5 w-5", CONTACTS_PANEL_MUTED_ICON_CLASS)} />}>
|
|
<div>
|
|
<p className={cn("text-sm", CONTACTS_HEADING_TEXT)}>{contact.company}</p>
|
|
{contact.department && (
|
|
<p className={cn("text-xs", CONTACTS_MUTED_TEXT)}>{contact.department}</p>
|
|
)}
|
|
{contact.jobTitle && (
|
|
<p className={cn("text-xs", CONTACTS_MUTED_TEXT)}>{contact.jobTitle}</p>
|
|
)}
|
|
</div>
|
|
</DetailRow>
|
|
)}
|
|
|
|
{contact.addresses && contact.addresses.length > 0 && (
|
|
<DetailRow icon={<MapPin className={cn("h-5 w-5", CONTACTS_PANEL_MUTED_ICON_CLASS)} />}>
|
|
{contact.addresses.map((addr, i) => (
|
|
<div key={i}>
|
|
<p className={cn("text-sm", CONTACTS_HEADING_TEXT)}>
|
|
{[addr.street, [addr.postalCode, addr.city].filter(Boolean).join(" "), addr.region, addr.country]
|
|
.filter(Boolean)
|
|
.join(", ")}
|
|
</p>
|
|
<p className={cn("text-xs", CONTACTS_MUTED_TEXT)}>{addr.label}</p>
|
|
</div>
|
|
))}
|
|
</DetailRow>
|
|
)}
|
|
|
|
{contact.birthday && (contact.birthday.day || contact.birthday.month) && (
|
|
<DetailRow icon={<Cake className={cn("h-5 w-5", CONTACTS_PANEL_MUTED_ICON_CLASS)} />}>
|
|
<p className={cn("text-sm", CONTACTS_HEADING_TEXT)}>{formatBirthday(contact.birthday)}</p>
|
|
</DetailRow>
|
|
)}
|
|
|
|
{contact.notes && (
|
|
<DetailRow icon={<FileText className={cn("h-5 w-5", CONTACTS_PANEL_MUTED_ICON_CLASS)} />}>
|
|
<p className={cn("whitespace-pre-wrap text-sm", CONTACTS_HEADING_TEXT)}>{contact.notes}</p>
|
|
</DetailRow>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DetailRow({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex gap-4 py-3">
|
|
<div className="flex w-6 shrink-0 pt-0.5">{icon}</div>
|
|
<div className="flex-1 space-y-2">{children}</div>
|
|
</div>
|
|
)
|
|
}
|