ultisuite-client/components/gmail/contacts-page/contacts-sidebar.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- Updated .env.example to include configuration for OnlyOffice Document Server.
- Modified the workspace configuration to remove the drive-suite path.
- Adjusted TypeScript environment imports for consistency.
- Enhanced Next.js configuration to disable canvas in Webpack.
- Updated package.json to include new dependencies for OnlyOffice and PDF.js.
- Added global styles for OnlyOffice theme integration in the CSS.
- Created new layout and page components for the Drive feature, including public sharing and editing functionalities.
- Updated metadata handling across various layouts to reflect the new app structure.
2026-06-07 15:49:21 +02:00

341 lines
10 KiB
TypeScript

"use client"
import { useDeferredValue, useMemo, useState } from "react"
import {
Users,
Clock,
UserPlus,
Ban,
EyeOff,
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_HEADING_TEXT,
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 { CONTACTS_MENU_SURFACE_CLASS } from "@/lib/contacts-chrome-classes"
import { useContactsList } from "@/lib/contacts/use-contacts-list"
import { useContactsStore } from "@/lib/contacts/contacts-store"
import { useDiscoveryCounts, useVisibleEnrichmentSuggestions } from "@/lib/api/hooks/use-contact-discovery"
import { findDuplicatePairs } from "@/lib/contacts/duplicate-detection"
import { useNavStore } from "@/lib/stores/nav-store"
import { ContactsPanelLogo } from "@/components/gmail/contacts/contacts-panel-logo"
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 } = useContactsList()
const deferredContacts = useDeferredValue(contacts)
const ignoredMergePairs = useContactsStore((s) => s.ignoredMergePairs)
const { data: discoveryCounts } = useDiscoveryCounts()
const mergeSuggestionCount = useMemo(() => {
if (deferredContacts.length < 2) return 0
return findDuplicatePairs(deferredContacts, new Set(ignoredMergePairs)).length
}, [deferredContacts, ignoredMergePairs])
const { suggestions: coordinateSuggestions } = useVisibleEnrichmentSuggestions()
const otherContactsCount = discoveryCounts?.other_contacts ?? 0
const ignoredCount = discoveryCounts?.ignored ?? 0
const blockedCount = discoveryCounts?.blocked ?? 0
const coordinatesCount = coordinateSuggestions.length
const mergeBadgeCount = mergeSuggestionCount + coordinatesCount
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>
<ContactsPanelLogo
onClick={onHome ?? (() => onNavigate("contacts"))}
titleClassName={cn("text-[22px] font-normal", CONTACTS_HEADING_TEXT)}
/>
</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"
data-contacts-menu-surface
className={cn("w-56", CONTACTS_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"
badge={otherContactsCount > 0 ? otherContactsCount : undefined}
active={currentView === "other"}
onClick={() => onNavigate("other")}
/>
<NavItem
icon={<Ban className="h-5 w-5" />}
label="Bloqués"
count={blockedCount > 0 ? blockedCount : undefined}
active={currentView === "blocked"}
onClick={() => onNavigate("blocked")}
/>
<NavItem
icon={<EyeOff className="h-5 w-5" />}
label="Ignorés"
count={ignoredCount > 0 ? ignoredCount : undefined}
active={currentView === "ignored"}
onClick={() => onNavigate("ignored")}
/>
<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={mergeBadgeCount > 0 ? mergeBadgeCount : 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>
)
}