234 lines
7.3 KiB
TypeScript
234 lines
7.3 KiB
TypeScript
"use client"
|
|
|
|
import { 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 { useContactsStore } from "@/lib/contacts/contacts-store"
|
|
import { useNavStore } from "@/lib/stores/nav-store"
|
|
import type { ContactsPageView } from "./contacts-app-shell"
|
|
|
|
interface ContactsSidebarProps {
|
|
currentView: ContactsPageView
|
|
activeLabelId?: string | null
|
|
onNavigate: (view: ContactsPageView) => void
|
|
onCreateContact: () => void
|
|
onBulkCreate?: () => void
|
|
onSelectLabel?: (id: string) => void
|
|
}
|
|
|
|
export function ContactsSidebar({
|
|
currentView,
|
|
activeLabelId,
|
|
onNavigate,
|
|
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 availableLabels = labelRows.filter((r) => r.enabled !== false)
|
|
|
|
function handleAddLabel() {
|
|
const trimmed = labelInput.trim()
|
|
if (trimmed) {
|
|
addLabelRowFromSidebar(trimmed)
|
|
setLabelInput("")
|
|
setShowLabelInput(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<aside className="flex h-full w-60 shrink-0 flex-col border-r border-gray-200 bg-white">
|
|
{/* Logo + hamburger */}
|
|
<div className="flex h-16 items-center gap-2 px-4">
|
|
<Button variant="ghost" size="icon" className="h-10 w-10 rounded-full text-gray-600">
|
|
<Menu className="h-5 w-5" />
|
|
</Button>
|
|
<div className="flex items-center gap-2">
|
|
<Users className="h-6 w-6 text-[#5f6368]" />
|
|
<span className="text-[22px] font-normal text-[#5f6368]">Contacts</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Create button */}
|
|
<div className="px-3 pb-3">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex h-14 w-full items-center gap-3 rounded-2xl bg-white px-4 shadow-md ring-1 ring-gray-200 transition-shadow hover:shadow-lg"
|
|
>
|
|
<Plus className="h-5 w-5 text-[#1a73e8]" />
|
|
<span className="flex-1 text-left text-sm font-medium text-[#3c4043]">Créer un contact</span>
|
|
<ChevronDown className="h-4 w-4 text-[#5f6368]" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-56">
|
|
<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 items */}
|
|
<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-gray-200" />
|
|
|
|
<p className="px-3 py-2 text-xs font-medium text-[#5f6368]">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-gray-200" />
|
|
|
|
{/* Labels section */}
|
|
<div className="flex items-center justify-between px-3 py-2">
|
|
<p className="text-xs font-medium text-[#5f6368]">Libellés</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowLabelInput(true)}
|
|
className="rounded-full p-1 text-[#5f6368] hover:bg-gray-100"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</button>
|
|
</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="flex-1 rounded border border-gray-300 px-2 py-1 text-sm outline-none focus:border-blue-500"
|
|
autoFocus
|
|
/>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleAddLabel}>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{availableLabels.map((label) => {
|
|
const count = contacts.filter((c) => c.labels?.includes(label.id)).length
|
|
return (
|
|
<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={`flex w-full items-center gap-3 rounded-full px-3 py-2 text-sm transition-colors ${
|
|
active
|
|
? "bg-[#c2e7ff] font-medium text-[#001d35]"
|
|
: "text-[#1f1f1f] hover:bg-gray-100"
|
|
}`}
|
|
>
|
|
<span className={active ? "text-[#001d35]" : "text-[#444746]"}>{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="text-xs text-[#5f6368]">{count}</span>
|
|
)}
|
|
</button>
|
|
)
|
|
}
|