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%
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