Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Move mail, compose, contacts, and accounts off mocks onto REST + WS. Add client, auth store, IDB-backed query cache, offline queue, and sync bar; hybrid Zustand for UI-only state. Settings still local until backend has preferences API.
167 lines
4.5 KiB
TypeScript
167 lines
4.5 KiB
TypeScript
"use client"
|
|
|
|
import { create } from "zustand"
|
|
import { persist } from "zustand/middleware"
|
|
import { debouncedPersistJSONStorage } from "@/lib/stores/debounced-json-storage"
|
|
import type { FullContact } from "./types"
|
|
|
|
type ContactsView = "list" | "view" | "create" | "edit"
|
|
|
|
export type ContactCreateDraft = {
|
|
firstName?: string
|
|
lastName?: string
|
|
emails?: { value: string; label: string }[]
|
|
}
|
|
|
|
export interface DeletedContact {
|
|
contact: FullContact
|
|
deletedAt: number
|
|
reason: string
|
|
}
|
|
|
|
interface ContactsState {
|
|
deletedContacts: DeletedContact[]
|
|
ignoredMergePairs: string[]
|
|
panelOpen: boolean
|
|
view: ContactsView
|
|
activeContactId: string | null
|
|
searchQuery: string
|
|
searchMode: boolean
|
|
createDraft: ContactCreateDraft | null
|
|
}
|
|
|
|
interface ContactsActions {
|
|
togglePanel: () => void
|
|
openPanel: () => void
|
|
closePanel: () => void
|
|
openContactDetail: (contactId: string) => void
|
|
openCreateContact: (draft?: ContactCreateDraft | null) => void
|
|
clearCreateDraft: () => void
|
|
setView: (view: ContactsView, activeContactId?: string | null) => void
|
|
showContactsList: () => void
|
|
setSearchQuery: (q: string) => void
|
|
setSearchMode: (active: boolean) => void
|
|
softDeleteContact: (contact: FullContact, reason?: string) => void
|
|
restoreContact: (id: string) => void
|
|
emptyTrash: () => void
|
|
ignoreMergePair: (idA: string, idB: string) => void
|
|
}
|
|
|
|
export type ContactsStore = ContactsState & ContactsActions
|
|
|
|
export const useContactsStore = create<ContactsStore>()(
|
|
persist(
|
|
(set) => ({
|
|
deletedContacts: [],
|
|
ignoredMergePairs: [],
|
|
panelOpen: false,
|
|
view: "list",
|
|
activeContactId: null,
|
|
searchQuery: "",
|
|
searchMode: false,
|
|
createDraft: null,
|
|
|
|
togglePanel: () =>
|
|
set((s) =>
|
|
s.panelOpen
|
|
? {
|
|
panelOpen: false,
|
|
view: "list",
|
|
activeContactId: null,
|
|
searchQuery: "",
|
|
searchMode: false,
|
|
createDraft: null,
|
|
}
|
|
: { panelOpen: true }
|
|
),
|
|
|
|
openPanel: () => set({ panelOpen: true }),
|
|
|
|
closePanel: () =>
|
|
set({
|
|
panelOpen: false,
|
|
view: "list",
|
|
activeContactId: null,
|
|
searchQuery: "",
|
|
searchMode: false,
|
|
createDraft: null,
|
|
}),
|
|
|
|
openContactDetail: (contactId) =>
|
|
set({
|
|
panelOpen: true,
|
|
view: "view",
|
|
activeContactId: contactId,
|
|
searchQuery: "",
|
|
searchMode: false,
|
|
createDraft: null,
|
|
}),
|
|
|
|
openCreateContact: (draft = null) =>
|
|
set({
|
|
panelOpen: true,
|
|
view: "create",
|
|
activeContactId: null,
|
|
searchQuery: "",
|
|
searchMode: false,
|
|
createDraft: draft,
|
|
}),
|
|
|
|
clearCreateDraft: () => set({ createDraft: null }),
|
|
|
|
setView: (view, activeContactId = null) =>
|
|
set({ view, activeContactId, createDraft: null }),
|
|
|
|
showContactsList: () =>
|
|
set({
|
|
view: "list",
|
|
activeContactId: null,
|
|
searchQuery: "",
|
|
searchMode: false,
|
|
createDraft: null,
|
|
}),
|
|
|
|
setSearchQuery: (searchQuery) => set({ searchQuery }),
|
|
|
|
setSearchMode: (searchMode) =>
|
|
set(searchMode ? { searchMode } : { searchMode, searchQuery: "" }),
|
|
|
|
softDeleteContact: (contact, reason = "Supprimé manuellement") =>
|
|
set((s) => ({
|
|
deletedContacts: [
|
|
...s.deletedContacts,
|
|
{ contact, deletedAt: Date.now(), reason },
|
|
],
|
|
activeContactId: s.activeContactId === contact.id ? null : s.activeContactId,
|
|
view: s.activeContactId === contact.id ? "list" : s.view,
|
|
})),
|
|
|
|
restoreContact: (id) =>
|
|
set((s) => {
|
|
const entry = s.deletedContacts.find((d) => d.contact.id === id)
|
|
if (!entry) return s
|
|
return {
|
|
deletedContacts: s.deletedContacts.filter((d) => d.contact.id !== id),
|
|
}
|
|
}),
|
|
|
|
emptyTrash: () => set({ deletedContacts: [] }),
|
|
|
|
ignoreMergePair: (idA, idB) =>
|
|
set((s) => {
|
|
const key = [idA, idB].sort().join("::")
|
|
if (s.ignoredMergePairs.includes(key)) return s
|
|
return { ignoredMergePairs: [...s.ignoredMergePairs, key] }
|
|
}),
|
|
}),
|
|
{
|
|
name: "contacts-store",
|
|
storage: debouncedPersistJSONStorage,
|
|
partialize: (state) => ({
|
|
deletedContacts: state.deletedContacts,
|
|
ignoredMergePairs: state.ignoredMergePairs,
|
|
}),
|
|
}
|
|
)
|
|
)
|