175 lines
5.4 KiB
TypeScript
175 lines
5.4 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 { searchContacts } from "@/lib/contacts/fuzzy-search"
|
|
import { fullContactDisplayName } from "@/lib/contacts/types"
|
|
import { ContactRow } from "./contact-row"
|
|
|
|
export function ContactsListView() {
|
|
const {
|
|
contacts,
|
|
searchMode,
|
|
searchQuery,
|
|
setSearchMode,
|
|
setSearchQuery,
|
|
setView,
|
|
closePanel,
|
|
} = useContactsStore()
|
|
|
|
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() {
|
|
setSearchQuery("")
|
|
setSearchMode(false)
|
|
}
|
|
|
|
if (searchMode) {
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<div className="flex h-12 shrink-0 items-center gap-2 border-b border-gray-200 px-4">
|
|
<Search className="h-4 w-4 shrink-0 text-gray-500" />
|
|
<input
|
|
ref={searchInputRef}
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Recherche..."
|
|
className="flex-1 bg-transparent text-sm outline-none placeholder:text-gray-400"
|
|
/>
|
|
<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="flex h-full flex-col">
|
|
<div className="flex h-12 shrink-0 items-center justify-between border-b border-gray-200 px-4">
|
|
<span className="text-lg font-medium text-gray-900">Contacts</span>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 rounded-full text-gray-600"
|
|
onClick={() => setSearchMode(true)}
|
|
>
|
|
<Search className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 rounded-full text-gray-600"
|
|
asChild
|
|
>
|
|
<Link href="/contacts">
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 rounded-full text-gray-600"
|
|
onClick={closePanel}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<ScrollArea className="min-h-0 flex-1">
|
|
<CreateContactButton onClick={() => setView("create")} />
|
|
<div className="px-4 py-2 text-xs font-medium text-gray-500">
|
|
Contacts ({contacts.length})
|
|
</div>
|
|
{groupedContacts.map((group) => (
|
|
<div key={group.letter}>
|
|
<div className="px-4 py-1 text-xs font-medium uppercase text-gray-500">
|
|
{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="flex w-full items-center gap-3 px-4 h-12 hover:bg-gray-50 cursor-pointer"
|
|
>
|
|
<div className="flex h-10 w-10 items-center justify-center">
|
|
<Plus className="h-5 w-5 text-[#1a73e8]" />
|
|
</div>
|
|
<span className="text-sm font-medium text-[#1a73e8]">Créer un contact</span>
|
|
</button>
|
|
)
|
|
}
|