ultisuite-client/components/gmail/contacts-page/contacts-sidebar.tsx
R3D347HR4Y 9266aa34cd huhu
2026-05-19 22:20:43 +02:00

307 lines
8.9 KiB
TypeScript

"use client"
import { useMemo, useState } from "react"
import {
Users,
Clock,
UserPlus,
Merge,
Upload,
Trash2,
Plus,
Tag,
Menu,
ChevronDown,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
import {
CONTACTS_CREATE_BTN_CLASS,
CONTACTS_FIELD_CLASS,
CONTACTS_MUTED_TEXT,
CONTACTS_NAV_ACTIVE_CLASS,
CONTACTS_NAV_ICON_MUTED,
CONTACTS_NAV_ITEM_CLASS,
CONTACTS_CREATE_BTN_LABEL_CLASS,
CONTACTS_SIDEBAR_CLASS,
} from "@/lib/contacts-chrome-classes"
import { MAIL_SIDEBAR_MENU_SURFACE_CLASS } from "@/lib/mail-chrome-classes"
import { useContactsStore } from "@/lib/contacts/contacts-store"
import { useNavStore } from "@/lib/stores/nav-store"
import type { ContactsPageView } from "./contacts-app-shell"
interface ContactsSidebarProps {
open: boolean
overlay: boolean
onToggle: () => void
onClose: () => void
currentView: ContactsPageView
activeLabelId?: string | null
onNavigate: (view: ContactsPageView) => void
onHome?: () => void
onCreateContact: () => void
onBulkCreate?: () => void
onSelectLabel?: (id: string) => void
}
export function ContactsSidebar({
open,
overlay,
onToggle,
onClose,
currentView,
activeLabelId,
onNavigate,
onHome,
onCreateContact,
onBulkCreate,
onSelectLabel,
}: ContactsSidebarProps) {
const contacts = useContactsStore((s) => s.contacts)
const mergeSuggestionCount = useContactsStore((s) => s.getMergeSuggestions().length)
const labelRows = useNavStore((s) => s.labelRows)
const addLabelRowFromSidebar = useNavStore((s) => s.addLabelRowFromSidebar)
const [labelInput, setLabelInput] = useState("")
const [showLabelInput, setShowLabelInput] = useState(false)
const labelsByContactCount = useMemo(() => {
return labelRows
.filter((r) => r.enabled !== false)
.map((label) => ({
label,
count: contacts.filter((c) => c.labels?.includes(label.id)).length,
}))
.sort(
(a, b) =>
b.count - a.count ||
a.label.label.localeCompare(b.label.label, "fr")
)
}, [labelRows, contacts])
function handleAddLabel() {
const trimmed = labelInput.trim()
if (trimmed) {
addLabelRowFromSidebar(trimmed)
setLabelInput("")
setShowLabelInput(false)
}
}
function handleMenuClick() {
if (overlay && open) {
onClose()
} else {
onToggle()
}
}
if (!overlay && !open) {
return null
}
return (
<aside
className={cn(
CONTACTS_SIDEBAR_CLASS,
overlay
? cn(
"fixed inset-y-0 left-0 z-50 shadow-xl",
open ? "translate-x-0" : "-translate-x-full pointer-events-none"
)
: "relative"
)}
aria-hidden={overlay && !open}
>
<div className="flex h-16 items-center gap-2 px-4">
<Button
variant="ghost"
size="icon"
className="h-10 w-10 rounded-full text-muted-foreground hover:bg-accent"
onClick={handleMenuClick}
aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}
>
<Menu className="h-5 w-5" />
</Button>
<button
type="button"
onClick={onHome ?? (() => onNavigate("contacts"))}
className="flex min-w-0 items-center gap-2 rounded-full px-1 py-0.5 transition-colors hover:bg-accent"
aria-label="Liste des contacts"
>
<Users className={cn("h-6 w-6", CONTACTS_NAV_ICON_MUTED)} />
<span className={cn("text-[22px] font-normal", CONTACTS_MUTED_TEXT)}>Contacts</span>
</button>
</div>
{/* Create button */}
<div className="px-3 pb-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={CONTACTS_CREATE_BTN_CLASS}
>
<Plus className="h-5 w-5 text-primary" />
<span className={CONTACTS_CREATE_BTN_LABEL_CLASS}>Créer un contact</span>
<ChevronDown className={cn("h-4 w-4", CONTACTS_NAV_ICON_MUTED)} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className={cn("w-56", MAIL_SIDEBAR_MENU_SURFACE_CLASS)}>
<DropdownMenuItem onClick={onCreateContact}>
<UserPlus className="mr-2 h-4 w-4" />
Créer un contact
</DropdownMenuItem>
<DropdownMenuItem onClick={onBulkCreate}>
<Users className="mr-2 h-4 w-4" />
Créer plusieurs contacts
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<nav className="flex-1 overflow-y-auto px-2">
<NavItem
icon={<Users className="h-5 w-5" />}
label="Contacts"
count={contacts.length}
active={currentView === "contacts"}
onClick={() => onNavigate("contacts")}
/>
<NavItem
icon={<Clock className="h-5 w-5" />}
label="Fréquents"
active={currentView === "frequent"}
onClick={() => onNavigate("frequent")}
/>
<NavItem
icon={<UserPlus className="h-5 w-5" />}
label="Autres contacts"
active={currentView === "other"}
onClick={() => onNavigate("other")}
/>
<div className="my-2 border-t border-border" />
<p className={cn("px-3 py-2 text-xs font-medium", CONTACTS_MUTED_TEXT)}>Corriger et gérer</p>
<NavItem
icon={<Merge className="h-5 w-5" />}
label="Fusionner et corriger"
badge={mergeSuggestionCount > 0 ? mergeSuggestionCount : undefined}
active={currentView === "merge"}
onClick={() => onNavigate("merge")}
/>
<NavItem
icon={<Upload className="h-5 w-5" />}
label="Importer"
active={currentView === "import"}
onClick={() => onNavigate("import")}
/>
<NavItem
icon={<Trash2 className="h-5 w-5" />}
label="Corbeille"
active={currentView === "trash"}
onClick={() => onNavigate("trash")}
/>
<div className="my-2 border-t border-border" />
<div className="flex items-center gap-3 px-3 py-2">
<p className={cn("min-w-0 flex-1 text-xs font-medium", CONTACTS_MUTED_TEXT)}>
Libellés
</p>
<div className="flex w-6 shrink-0 justify-center">
<button
type="button"
onClick={() => setShowLabelInput(true)}
className="rounded-full p-1 text-muted-foreground hover:bg-accent"
aria-label="Ajouter un libellé"
>
<Plus className="h-4 w-4" />
</button>
</div>
</div>
{showLabelInput && (
<div className="flex items-center gap-1 px-3 pb-2">
<input
type="text"
value={labelInput}
onChange={(e) => setLabelInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAddLabel()}
placeholder="Nom du libellé"
className={cn("flex-1", CONTACTS_FIELD_CLASS)}
autoFocus
/>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleAddLabel}>
<Plus className="h-4 w-4" />
</Button>
</div>
)}
{labelsByContactCount.map(({ label, count }) => (
<NavItem
key={label.id}
icon={<Tag className="h-5 w-5" />}
label={label.label}
count={count}
active={currentView === "label" && activeLabelId === label.id}
onClick={() => onSelectLabel?.(label.id)}
/>
))}
</nav>
</aside>
)
}
function NavItem({
icon,
label,
count,
badge,
active,
onClick,
}: {
icon: React.ReactNode
label: string
count?: number
badge?: number
active: boolean
onClick: () => void
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
"flex w-full items-center gap-3 rounded-full px-3 py-2 text-sm transition-colors",
active ? CONTACTS_NAV_ACTIVE_CLASS : CONTACTS_NAV_ITEM_CLASS
)}
>
<span className={active ? "text-mail-nav-selected" : CONTACTS_NAV_ICON_MUTED}>{icon}</span>
<span className="flex-1 truncate text-left">{label}</span>
{badge !== undefined && (
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-[#ea4335] px-1.5 text-[11px] font-medium text-white">
{badge}
</span>
)}
{count !== undefined && (
<span
className={cn(
"flex w-6 shrink-0 justify-center text-xs tabular-nums",
CONTACTS_MUTED_TEXT
)}
>
{count}
</span>
)}
</button>
)
}