SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
3 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
3.0 KB · 31 lines tsx
Raw Blame History
1"use client";2import { useEffect, useState } from "react";3import Link from "next/link";4import { useFavorites, useSavedSearches, useCompare } from "@/lib/favorites";5import type { ListingCard } from "@/lib/types";6import { ListingCardView } from "@/components/listing-card";7import { Empty, Notice } from "@/components/ui";8export function useCards(uids: string[]) {9  const [cards, setCards] = useState<ListingCard[]>([]);10  useEffect(() => { if (!uids.length) { setCards([]); return; } fetch(`/api/listings?uids=${encodeURIComponent(uids.join(","))}`).then((r) => r.json()).then((d) => setCards(d.items ?? [])).catch(() => setCards([])); }, [uids.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps11  return cards;12}13export function FavoritesView({ member = false }: { member?: boolean } = {}) {14  const fav = useFavorites(); const saved = useSavedSearches(); const cmp = useCompare(); const cards = useCards(fav.items);15  const gone = fav.items.filter((u) => cards.length && !cards.some((c) => c.uid === u));16  return (17    <div className="mx-auto max-w-7xl px-4 py-8">18      <div className="flex items-end justify-between gap-3"><h1 className="text-3xl font-extrabold tracking-tight">Mes favoris</h1>{cmp.items.length >= 2 && <Link href="/comparer" className="tv-btn tv-btn-ghost">Comparateur ({cmp.items.length})</Link>}</div>19      <p className="mt-1 text-sm text-ink-2">{member ? "Favoris synchronisés avec votre compte Toveli, disponibles sur tous vos appareils." : <>Sans compte, vos favoris restent sur cet appareil. <Link href="/connexion?next=/membre/favoris" className="underline">Connectez-vous</Link> pour les retrouver partout et créer des alertes.</>}</p>20      {fav.items.length === 0 ? <div className="mt-6"><Empty title="Aucun favori pour l'instant" hint="Touchez le cœur sur une annonce pour la retrouver ici." action={{ href: "/recherche", label: "Explorer les annonces" }} /></div> : (21        <div className="mt-6 grid grid-cols-2 md:grid-cols-4 gap-3">{cards.map((c) => <ListingCardView key={c.uid} c={c} />)}</div>22      )}23      {gone.length > 0 && <div className="mt-3"><Notice>{gone.length} favori{gone.length > 1 ? "s ne sont" : " n'est"} plus en ligne (annonce retirée par la source).</Notice></div>}24      <section className="mt-10"><h2 className="text-xl font-bold mb-3">Recherches sauvegardées</h2>25        {saved.items.length === 0 ? <p className="text-sm text-ink-3">Sauvegardez une recherche depuis la page des résultats pour la relancer en un clic.</p> : <ul className="tv-card divide-y divide-line text-sm">{saved.items.map((s) => <li key={s.id} className="flex items-center justify-between gap-3 p-3"><Link href={s.href} className="font-medium hover:underline truncate">{s.label}</Link><button type="button" onClick={() => saved.remove(s.id)} className="text-xs text-ink-3 hover:text-brand">Retirer</button></li>)}</ul>}26        <Notice>Les alertes par courriel nécessiteront un compte ; cette première version garde tout en local.</Notice>27      </section>28    </div>29  );30}31