SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
4.2 KB · 54 lines typescript
Raw Blame History
1"use client";2// Favoris et comparateur sans compte : stockés localement (localStorage), supprimables par l'utilisateur. Aucune donnée n'est envoyée au serveur.3import { useCallback, useEffect, useState } from "react";45const KEY_FAV = "toveli:favorites:v1";6const KEY_CMP = "toveli:compare:v1";7const KEY_SEARCH = "toveli:searches:v1";89function read<T>(k: string, fallback: T): T { if (typeof window === "undefined") return fallback; try { const v = localStorage.getItem(k); return v ? (JSON.parse(v) as T) : fallback; } catch { return fallback; } }10function write(k: string, v: unknown) { try { localStorage.setItem(k, JSON.stringify(v)); window.dispatchEvent(new Event("toveli:storage")); } catch { /* quota */ } }1112function useLocalList(key: string, max: number) {13  const [items, setItems] = useState<string[]>([]);14  useEffect(() => { const load = () => setItems(read<string[]>(key, [])); load(); window.addEventListener("toveli:storage", load); window.addEventListener("storage", load); return () => { window.removeEventListener("toveli:storage", load); window.removeEventListener("storage", load); }; }, [key]);15  const toggle = useCallback((uid: string) => { const cur = read<string[]>(key, []); const next = cur.includes(uid) ? cur.filter((x) => x !== uid) : [uid, ...cur].slice(0, max); write(key, next); setItems(next); }, [key, max]);16  const has = useCallback((uid: string) => items.includes(uid), [items]);17  const clear = useCallback(() => { write(key, []); setItems([]); }, [key]);18  return { items, toggle, has, clear };19}20/** Favoris : locaux par défaut ; si l'utilisateur est connecté, synchronisés avec le serveur (les favoris locaux sont poussés une fois, puis le serveur fait foi). */21export function useFavorites() {22  const local = useLocalList(KEY_FAV, 200);23  const [server, setServer] = useState<string[] | null>(null);24  useEffect(() => {25    let alive = true;26    fetch("/api/me").then((r) => r.json()).then(async (d) => {27      if (!alive || !d?.user) return;28      const cur = read<string[]>(KEY_FAV, []);29      if (cur.length) { await fetch("/api/me/favorites", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ uids: cur }) }).catch(() => {}); write(KEY_FAV, []); }30      const r = await fetch("/api/me/favorites").then((r) => r.json()).catch(() => null);31      if (alive && r?.items) setServer(r.items.map((i: { uid: string }) => i.uid));32    }).catch(() => {});33    return () => { alive = false; };34  }, []);35  if (server === null) return local;36  return {37    items: server,38    has: (uid: string) => server.includes(uid),39    toggle: (uid: string) => { const next = server.includes(uid) ? server.filter((x) => x !== uid) : [uid, ...server]; setServer(next); fetch("/api/me/favorites", { method: server.includes(uid) ? "DELETE" : "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ uids: [uid] }) }).catch(() => {}); },40    clear: () => { fetch("/api/me/favorites", { method: "DELETE", headers: { "content-type": "application/json" }, body: JSON.stringify({ uids: server }) }).catch(() => {}); setServer([]); },41  };42}43export function useCompare() { return useLocalList(KEY_CMP, 4); }4445export interface SavedSearch { id: string; label: string; href: string; createdAt: string; }46export function useSavedSearches() {47  const [items, setItems] = useState<SavedSearch[]>([]);48  useEffect(() => { const load = () => setItems(read<SavedSearch[]>(KEY_SEARCH, [])); load(); window.addEventListener("toveli:storage", load); return () => window.removeEventListener("toveli:storage", load); }, []);49  const save = useCallback((label: string, href: string) => { const cur = read<SavedSearch[]>(KEY_SEARCH, []); if (cur.some((s) => s.href === href)) return; const next = [{ id: Math.random().toString(36).slice(2), label, href, createdAt: new Date().toISOString() }, ...cur].slice(0, 50); write(KEY_SEARCH, next); setItems(next); }, []);50  const remove = useCallback((id: string) => { const next = read<SavedSearch[]>(KEY_SEARCH, []).filter((s) => s.id !== id); write(KEY_SEARCH, next); setItems(next); }, []);51  const clearAll = useCallback(() => { [KEY_FAV, KEY_CMP, KEY_SEARCH].forEach((k) => localStorage.removeItem(k)); window.dispatchEvent(new Event("toveli:storage")); }, []);52  return { items, save, remove, clearAll };53}54