SPB Git forge

spb/trouve-ka

Public ★ Pinned

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

40commits 1branches 0releases
9.2 MBsize
maindefault branch
23 days agolast push
Python 51.9% TypeScript 32.7% JavaScript 4.8% CSS 4.1% Shell 2.9% HTML 1.8% SQL 1.5%
5.5 KB · 159 lines tsx
Raw Blame History
1/**2 * Trouve-KA — contenu client de la page « Mes favoris » (Mon univers Ka)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 *6 * Liste vivante des favoris du membre, lue au hub Groupe KA via7 * /api/favorites (magasin partagé du fav-store — mêmes cœurs que les8 * résultats). Retrait immédiat (toggle optimiste, le hub tranche).9 */1011"use client";1213import { useEffect } from "react";14import Link from "next/link";15import { Favicon } from "@/components/favicon";16import {17  ensureLoaded,18  toggleFav,19  useFavorites,20  type Fav,21} from "@/components/fav-store";2223function domainOf(url: string | undefined): string {24  if (!url) return "";25  try {26    return new URL(url).hostname.replace(/^www\./, "");27  } catch {28    return "";29  }30}3132function FavCard({ fav }: { fav: Fav }) {33  const domain = domainOf(fav.url);34  return (35    <article className="group flex items-start gap-4 py-6 sm:gap-6">36      <div className="min-w-0 flex-1">37        {domain ? (38          <div className="flex items-center gap-2">39            <Favicon domain={domain} />40            <span className="gk-mono truncate text-xs font-medium text-green">41              {fav.subtitle || domain}42            </span>43          </div>44        ) : null}45        <h3 className="mt-1.5 text-[17px] font-bold leading-snug tracking-[-0.02em] sm:text-lg">46          <a47            href={fav.url || "#"}48            className="rounded-sm text-ink underline decoration-transparent decoration-[3px] underline-offset-[3px] transition-colors hover:decoration-lime focus-visible:decoration-lime"49          >50            {fav.title || fav.url}51          </a>52        </h3>53        {fav.price_label ? (54          <p className="mt-1.5 text-sm font-medium text-ink-2">55            {fav.price_label}56          </p>57        ) : null}58        <button59          type="button"60          onClick={() =>61            void toggleFav({62              url: fav.url || fav.item_id,63              title: fav.title,64              subtitle: fav.subtitle,65              image_url: fav.image_url,66            })67          }68          className="gk-display mt-3 inline-flex min-h-[36px] items-center gap-1.5 rounded-full border-[1.5px] border-ink bg-surface px-3.5 text-[12px] font-bold text-ink transition-colors hover:bg-[var(--accent-soft)]"69        >70          <svg71            width="12"72            height="12"73            viewBox="0 0 24 24"74            fill="currentColor"75            aria-hidden="true"76          >77            <path d="M12 21c-4.8-3.6-9-6.9-9-11a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 4.1-4.2 7.4-9 11Z" />78          </svg>79          Retirer80        </button>81      </div>82      {fav.image_url ? (83        // eslint-disable-next-line @next/next/no-img-element84        <img85          src={fav.image_url}86          alt=""87          loading="lazy"88          className="h-[76px] w-[76px] shrink-0 rounded-card border border-line object-cover sm:h-[110px] sm:w-[110px]"89        />90      ) : null}91    </article>92  );93}9495export function FavorisContent({ name }: { name: string }) {96  const favs = useFavorites();9798  useEffect(() => {99    // Liste fraîche à l'arrivée sur la page (le cache serveur reste à 30 s).100    void ensureLoaded(true);101  }, []);102103  const loading = favs.status === "idle" || favs.status === "loading";104105  return (106    <main id="contenu" className="mx-auto w-full max-w-3xl flex-1 px-4 py-8">107      <p className="klabel">Mon univers Ka</p>108      <h1 className="mt-2 text-3xl font-bold leading-tight tracking-[-0.03em] sm:text-4xl">109        Mes favoris110      </h1>111      <p className="mt-3 max-w-xl text-sm leading-relaxed text-ink-2">112        {name ? `${name}, voici` : "Voici"} les pages que vous avez gardées —113        la même liste vous suit sur toutes les plateformes du Groupe KA.114      </p>115116      {loading ? (117        <div aria-hidden="true" className="mt-6 divide-y divide-line">118          {Array.from({ length: 3 }, (_, i) => (119            <div key={i} className="animate-pulse space-y-2.5 py-6">120              <div className="h-3.5 w-1/3 rounded bg-[var(--line)]" />121              <div className="h-5 w-2/3 rounded bg-[var(--line)]" />122              <div className="h-9 w-24 rounded-full bg-[var(--line)]" />123            </div>124          ))}125        </div>126      ) : favs.status === "error" ? (127        <div className="mt-10 text-center">128          <p className="font-display font-bold text-ink">129            Le hub Groupe KA est momentanément injoignable.130          </p>131          <p className="mt-2 text-sm text-ink-2">132            Vos favoris sont en sécurité — réessayez dans quelques instants.133          </p>134        </div>135      ) : favs.items.length === 0 ? (136        <div className="rise mt-10 py-10 text-center">137          <p className="klabel">0 favori</p>138          <h2 className="mx-auto mt-4 max-w-xl text-2xl font-bold leading-tight tracking-[-0.03em] sm:text-3xl">139            Rien dans votre univers — <span className="hl">encore</span>.140          </h2>141          <p className="mx-auto mt-5 max-w-md text-sm leading-relaxed text-ink-2">142            Lancez une recherche et touchez le cœur ♥ d&apos;un résultat pour143            le retrouver ici, et sur tous les sites du Groupe KA.144          </p>145          <Link href="/" className="btn btn-primary mt-8">146            Lancer une recherche147          </Link>148        </div>149      ) : (150        <div className="stagger mt-2 divide-y divide-line">151          {favs.items.map((fav) => (152            <FavCard key={fav.item_id || fav.url} fav={fav} />153          ))}154        </div>155      )}156    </main>157  );158}159