Files
SRCmail/app/[locale]/contacts/page.tsx
T
Matthieu MALVACHEandMatthieu MALVACHE 8a5bc9b88d feat: add address book, fix email layout, update dependencies
- Address book with JMAP sync and local fallback (contacts CRUD,
  search/filter, composer autocomplete, i18n for 8 languages)
- Fix email layout: remove horizontal scroll, left-side clipping,
  and empty spaces from blocked external images in newsletters
- Update all dependencies to latest compatible versions
- Expand i18n from 3 to 8 languages (added ES, IT, DE, NL, PT)
- Upgrade Next.js to 16.1.6 for security patches
2026-02-16 17:25:20 +01:00

171 lines
4.8 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useRouter } from "@/i18n/navigation";
import { useTranslations } from "next-intl";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ContactList } from "@/components/contacts/contact-list";
import { ContactDetail } from "@/components/contacts/contact-detail";
import { ContactForm } from "@/components/contacts/contact-form";
import { useContactStore } from "@/stores/contact-store";
import { useAuthStore } from "@/stores/auth-store";
import { toast } from "@/stores/toast-store";
import type { ContactCard } from "@/lib/jmap/types";
type View = "list" | "detail" | "create" | "edit";
export default function ContactsPage() {
const router = useRouter();
const t = useTranslations("contacts");
const { client, isAuthenticated } = useAuthStore();
const {
contacts,
selectedContactId,
searchQuery,
supportsSync,
setSelectedContact,
setSearchQuery,
fetchContacts,
createContact,
updateContact,
deleteContact,
addLocalContact,
updateLocalContact,
deleteLocalContact,
} = useContactStore();
const [view, setView] = useState<View>("list");
const hasFetched = useRef(false);
useEffect(() => {
if (!isAuthenticated) {
router.push("/login");
}
}, [isAuthenticated, router]);
useEffect(() => {
if (client && supportsSync && !hasFetched.current) {
hasFetched.current = true;
fetchContacts(client);
}
}, [client, supportsSync, fetchContacts]);
const selectedContact = contacts.find((c) => c.id === selectedContactId) || null;
const handleSelectContact = (id: string) => {
setSelectedContact(id);
setView("detail");
};
const handleCreateNew = () => {
setSelectedContact(null);
setView("create");
};
const handleEdit = () => {
setView("edit");
};
const handleDelete = async () => {
if (!selectedContact) return;
if (!window.confirm(t("delete_confirm"))) return;
try {
if (supportsSync && client) {
await deleteContact(client, selectedContact.id);
} else {
deleteLocalContact(selectedContact.id);
}
toast.success(t("toast.deleted"));
setView("list");
} catch {
toast.error(t("toast.error_delete"));
}
};
const handleSaveNew = useCallback(async (data: Partial<ContactCard>) => {
if (supportsSync && client) {
await createContact(client, data);
toast.success(t("toast.created"));
} else {
const localContact: ContactCard = {
id: `local-${crypto.randomUUID()}`,
addressBookIds: {},
...data,
};
addLocalContact(localContact);
toast.success(t("toast.created"));
}
setView("list");
}, [supportsSync, client, createContact, addLocalContact, t]);
const handleSaveEdit = useCallback(async (data: Partial<ContactCard>) => {
if (!selectedContact) return;
if (supportsSync && client) {
await updateContact(client, selectedContact.id, data);
toast.success(t("toast.updated"));
} else {
updateLocalContact(selectedContact.id, data);
toast.success(t("toast.updated"));
}
setView("detail");
}, [supportsSync, client, selectedContact, updateContact, updateLocalContact, t]);
const handleCancel = () => {
setView(selectedContact ? "detail" : "list");
};
if (!isAuthenticated) return null;
return (
<div className="flex h-screen bg-background">
<div className="w-80 border-r border-border flex flex-col">
<div className="p-4 border-b border-border">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/")}
className="w-full justify-start"
>
<ArrowLeft className="w-4 h-4 mr-2" />
{t("back_to_mail")}
</Button>
</div>
<ContactList
contacts={contacts}
selectedContactId={selectedContactId}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
onSelectContact={handleSelectContact}
onCreateNew={handleCreateNew}
supportsSync={supportsSync}
className="flex-1"
/>
</div>
<div className="flex-1">
{view === "create" && (
<ContactForm onSave={handleSaveNew} onCancel={handleCancel} />
)}
{view === "edit" && selectedContact && (
<ContactForm
contact={selectedContact}
onSave={handleSaveEdit}
onCancel={handleCancel}
/>
)}
{(view === "list" || view === "detail") && (
<ContactDetail
contact={selectedContact}
onEdit={handleEdit}
onDelete={handleDelete}
/>
)}
</div>
</div>
);
}