SPB Git

spb/trouve-ka Public

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

Python 76.8% TypeScript 15.7% SQL 3.9% Shell 1.4% CSS 1.3% Dockerfile 0.7%
3.4 KB · 99 lines tsx
Raw Blame History
1/**2 * Trouve-KA — un résultat de recherche (favicon, titre, extrait, vignette, pastilles)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 */67import { Favicon } from "@/components/favicon";8import { ResultThumbnail } from "@/components/result-thumbnail";9import { Snippet } from "@/components/snippet";10import { Badge } from "@/components/ui/badge";11import { formatDate } from "@/lib/format";12import type { SearchResult } from "@/lib/types";1314function badgeVariant(15  label: string,16): "quebec" | "government" | "news" | "education" | "default" {17  if (label === "Québec") return "quebec";18  if (label === "Gouvernement") return "government";19  if (label === "Actualités") return "news";20  if (label === "Éducation") return "education";21  return "default";22}2324/** Intensité québécoise de la pastille (◆ / ◆◆ / ◆◆◆), sobre et graduée. */25function quebecMarks(score: number): string {26  if (score >= 0.85) return "◆◆◆";27  if (score >= 0.65) return "◆◆";28  return "◆";29}3031interface ResultItemProps {32  result: SearchResult;33}3435export function ResultItem({ result }: ResultItemProps) {36  const publishedAt = result.published_at37    ? formatDate(result.published_at)38    : "";39  const hasBadges = Boolean(result.badges && result.badges.length > 0);4041  return (42    <article className="group flex items-start gap-4 py-6 sm:gap-6">43      <div className="min-w-0 flex-1">44        <div className="flex items-center gap-2">45          <Favicon domain={result.domain} />46          <span className="gk-mono truncate text-xs font-medium text-green">47            {result.display_url || result.domain}48          </span>49        </div>50        <h3 className="mt-1.5 text-[17px] font-bold leading-snug tracking-[-0.02em] sm:text-lg">51          <a52            href={result.url}53            className="rounded-sm text-ink underline decoration-transparent decoration-[3px] underline-offset-[3px] transition-colors hover:decoration-lime focus-visible:decoration-lime"54          >55            {result.title || result.url}56          </a>57        </h3>58        <Snippet59          html={result.snippet}60          className="mt-1.5 text-sm leading-relaxed text-ink-2"61        />62        {publishedAt || hasBadges ? (63          <div className="mt-2.5 flex flex-wrap items-center gap-x-2 gap-y-1.5">64            {result.badges?.map((badge, index) => (65              <Badge66                key={badge}67                variant={badgeVariant(badge)}68                title={69                  badge === "Québec" && result.quebec_score >= 0.4570                    ? `Score Québec ${Math.round(result.quebec_score * 100)} %`71                    : undefined72                }73                className={index === 0 ? "-rotate-1 hover:rotate-0" : undefined}74              >75                {badge === "Québec" ? (76                  <span aria-hidden="true" className="text-[8px] leading-none">77                    {result.quebec_score >= 0.4578                      ? quebecMarks(result.quebec_score)79                      : "◆"}80                  </span>81                ) : null}82                {badge}83              </Badge>84            ))}85            {publishedAt ? (86              <span className="klabel normal-case tracking-[0.04em]">87                {publishedAt}88              </span>89            ) : null}90          </div>91        ) : null}92      </div>93      {result.image ? (94        <ResultThumbnail src={result.image} href={result.url} />95      ) : null}96    </article>97  );98}99