"use client"; import { useMemo, useState, useCallback, type DragEvent } from "react"; import { useTranslations } from "next-intl"; import { BookUser, Users, Plus, UserPlus, Share2, Book } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { ContactCard, AddressBook } from "@/lib/jmap/types"; import { getContactDisplayName } from "@/stores/contact-store"; export type ContactCategory = "all" | { groupId: string } | { addressBookId: string }; interface ContactsSidebarProps { groups: ContactCard[]; individuals: ContactCard[]; addressBooks: AddressBook[]; activeCategory: ContactCategory; onSelectCategory: (category: ContactCategory) => void; onCreateGroup: () => void; onCreateContact: () => void; onDropContacts?: (contactIds: string[], addressBook: AddressBook) => void; className?: string; } export function ContactsSidebar({ groups, individuals, addressBooks, activeCategory, onSelectCategory, onCreateGroup, onCreateContact, onDropContacts, className, }: ContactsSidebarProps) { const t = useTranslations("contacts"); const sortedGroups = useMemo(() => { return [...groups].sort((a, b) => getContactDisplayName(a).localeCompare(getContactDisplayName(b)) ); }, [groups]); const isAllActive = activeCategory === "all"; // Group address books: personal vs shared accounts const personalBooks = useMemo(() => addressBooks.filter(b => !b.isShared), [addressBooks]); const sharedBookGroups = useMemo(() => { const map = new Map(); for (const book of addressBooks) { if (!book.isShared || !book.accountId) continue; const existing = map.get(book.accountId); if (existing) { existing.books.push(book); } else { map.set(book.accountId, { accountId: book.accountId, accountName: book.accountName || book.accountId, books: [book], }); } } return Array.from(map.values()); }, [addressBooks]); // Count contacts per address book const contactCountByBook = useMemo(() => { const counts: Record = {}; for (const contact of individuals) { if (!contact.addressBookIds) continue; for (const bookId of Object.keys(contact.addressBookIds)) { if (!contact.addressBookIds[bookId]) continue; // Build the full namespaced key const key = contact.isShared && contact.accountId ? `${contact.accountId}:${bookId}` : bookId; counts[key] = (counts[key] || 0) + 1; } } return counts; }, [individuals]); return (
{/* Header */}
{t("title")}
{/* Categories */}
{/* All contacts */} {/* Personal address books */} {personalBooks.length > 0 && (
{t("address_books.title")}
{personalBooks.map((book) => ( onSelectCategory({ addressBookId: book.id })} onDropContacts={onDropContacts} /> ))}
)} {/* Groups section */} {(sortedGroups.length > 0) && (
{t("tabs.groups")}
{sortedGroups.map((group) => { const isActive = typeof activeCategory === "object" && "groupId" in activeCategory && activeCategory.groupId === group.id; const memberCount = group.members ? Object.values(group.members).filter(Boolean).length : 0; return ( ); })}
)} {sortedGroups.length === 0 && (
{t("tabs.groups")}
)} {/* Shared accounts with address books */} {sharedBookGroups.map((group) => (
{group.accountName}
{group.books.map((book) => ( onSelectCategory({ addressBookId: book.id })} onDropContacts={onDropContacts} /> ))}
))}
); } function AddressBookItem({ book, isActive, contactCount, onSelect, onDropContacts, }: { book: AddressBook; isActive: boolean; contactCount: number; onSelect: () => void; onDropContacts?: (contactIds: string[], addressBook: AddressBook) => void; }) { const [isDragOver, setIsDragOver] = useState(false); const handleDragOver = useCallback((e: DragEvent) => { if (!e.dataTransfer.types.includes("application/x-contact-ids")) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; setIsDragOver(true); }, []); const handleDragLeave = useCallback(() => { setIsDragOver(false); }, []); const handleDrop = useCallback((e: DragEvent) => { e.preventDefault(); setIsDragOver(false); const data = e.dataTransfer.getData("application/x-contact-ids"); if (!data || !onDropContacts) return; try { const contactIds = JSON.parse(data) as string[]; if (contactIds.length > 0) { onDropContacts(contactIds, book); } } catch { // ignore invalid data } }, [book, onDropContacts]); return ( ); }