ultisuite-client/components/gmail/contacts/contacts-list-view.tsx
2026-05-25 13:52:40 +02:00

189 lines
5.8 KiB
TypeScript

"use client"
import { useRef, useEffect, useMemo } from "react"
import Link from "next/link"
import { Search, ExternalLink, X, Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useContactsStore } from "@/lib/contacts/contacts-store"
import { useContactsList } from "@/lib/contacts/use-contacts-list"
import { searchContacts } from "@/lib/contacts/fuzzy-search"
import { fullContactDisplayName } from "@/lib/contacts/types"
import {
CONTACTS_PANEL_CREATE_ROW_CLASS,
CONTACTS_PANEL_HEADER_CLASS,
CONTACTS_PANEL_HEADER_SEARCH_CLASS,
CONTACTS_PANEL_ICON_BTN_CLASS,
CONTACTS_PANEL_LETTER_CLASS,
CONTACTS_PANEL_LINK_TEXT_CLASS,
CONTACTS_PANEL_MUTED_ICON_CLASS,
CONTACTS_PANEL_SEARCH_INPUT_CLASS,
CONTACTS_PANEL_SECTION_LABEL_CLASS,
CONTACTS_PANEL_SHELL_CLASS,
} from "@/lib/contacts-chrome-classes"
import { cn } from "@/lib/utils"
import { ContactRow } from "./contact-row"
import { ContactsPanelLogo } from "./contacts-panel-logo"
export function ContactsListView() {
const {
searchMode,
searchQuery,
setSearchMode,
setSearchQuery,
setView,
showContactsList,
closePanel,
} = useContactsStore()
const { contacts } = useContactsList()
const searchInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (searchMode) {
searchInputRef.current?.focus()
}
}, [searchMode])
const filteredContacts = useMemo(() => {
if (searchMode && searchQuery) {
return searchContacts(contacts, searchQuery)
}
return contacts
}, [contacts, searchMode, searchQuery])
const groupedContacts = useMemo(() => {
const sorted = [...filteredContacts].sort((a, b) => {
const nameA = fullContactDisplayName(a) || a.emails[0]?.value || ""
const nameB = fullContactDisplayName(b) || b.emails[0]?.value || ""
return nameA.localeCompare(nameB, "fr")
})
const normalize = (ch: string) =>
ch.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase() || "?"
const groups: { letter: string; items: typeof sorted }[] = []
for (const contact of sorted) {
const name = fullContactDisplayName(contact) || contact.emails[0]?.value || "?"
const letter = normalize(name.charAt(0))
const last = groups[groups.length - 1]
if (last && last.letter === letter) {
last.items.push(contact)
} else {
groups.push({ letter, items: [contact] })
}
}
return groups
}, [filteredContacts])
function exitSearch() {
showContactsList()
}
if (searchMode) {
return (
<div className={CONTACTS_PANEL_SHELL_CLASS}>
<div className={cn(CONTACTS_PANEL_HEADER_SEARCH_CLASS, "gap-2")}>
<ContactsPanelLogo onClick={exitSearch} className="-ml-1 shrink-0" />
<Search className={`h-4 w-4 shrink-0 ${CONTACTS_PANEL_MUTED_ICON_CLASS}`} />
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Recherche..."
className={CONTACTS_PANEL_SEARCH_INPUT_CLASS}
/>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 rounded-full"
onClick={exitSearch}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="min-h-0 flex-1">
<CreateContactButton onClick={() => setView("create")} />
{filteredContacts.map((contact) => (
<ContactRow
key={contact.id}
contact={contact}
onClick={() => setView("view", contact.id)}
/>
))}
</ScrollArea>
</div>
)
}
return (
<div className={CONTACTS_PANEL_SHELL_CLASS}>
<div className={CONTACTS_PANEL_HEADER_CLASS}>
<ContactsPanelLogo onClick={showContactsList} className="-ml-1" />
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className={CONTACTS_PANEL_ICON_BTN_CLASS}
onClick={() => setSearchMode(true)}
>
<Search className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className={CONTACTS_PANEL_ICON_BTN_CLASS}
asChild
>
<Link href="/contacts">
<ExternalLink className="h-4 w-4" />
</Link>
</Button>
<Button
variant="ghost"
size="icon"
className={CONTACTS_PANEL_ICON_BTN_CLASS}
onClick={closePanel}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<CreateContactButton onClick={() => setView("create")} />
<div className={CONTACTS_PANEL_SECTION_LABEL_CLASS}>
Contacts ({contacts.length})
</div>
{groupedContacts.map((group) => (
<div key={group.letter}>
<div className={CONTACTS_PANEL_LETTER_CLASS}>{group.letter}</div>
{group.items.map((contact) => (
<ContactRow
key={contact.id}
contact={contact}
onClick={() => setView("view", contact.id)}
/>
))}
</div>
))}
</ScrollArea>
</div>
)
}
function CreateContactButton({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
className={CONTACTS_PANEL_CREATE_ROW_CLASS}
>
<div className="flex h-10 w-10 items-center justify-center">
<Plus className="h-5 w-5 text-primary" />
</div>
<span className={CONTACTS_PANEL_LINK_TEXT_CLASS}>Créer un contact</span>
</button>
)
}