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 — barre de recherche (homepage et en-tête des résultats)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 */67"use client";89import { useRouter } from "next/navigation";10import { useEffect, useState, type FormEvent } from "react";11import { cn } from "@/lib/utils";1213interface SearchBarProps {14 initialQuery?: string;15 autoFocus?: boolean;16 size?: "md" | "lg";17 /** `inline` : bouton dans la barre; `below` : grand bouton sous la barre. */18 buttonPlacement?: "inline" | "below";19 /** Suggestions québécoises affichées en placeholder, en rotation douce. */20 placeholderSuggestions?: string[];21 className?: string;22}2324const SUGGESTION_INTERVAL_MS = 4000;2526export function SearchBar({27 initialQuery = "",28 autoFocus = false,29 size = "md",30 buttonPlacement = "inline",31 placeholderSuggestions,32 className,33}: SearchBarProps) {34 const router = useRouter();35 const [query, setQuery] = useState(initialQuery);36 const [suggestionIndex, setSuggestionIndex] = useState(0);3738 const suggestionCount = placeholderSuggestions?.length ?? 0;3940 useEffect(() => {41 if (suggestionCount < 2) return;42 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;43 const timer = window.setInterval(44 () => setSuggestionIndex((index) => (index + 1) % suggestionCount),45 SUGGESTION_INTERVAL_MS,46 );47 return () => window.clearInterval(timer);48 }, [suggestionCount]);4950 const placeholder =51 suggestionCount > 052 ? `Essaie « ${placeholderSuggestions![suggestionIndex % suggestionCount]} »…`53 : "Rechercher…";5455 function onSubmit(event: FormEvent) {56 event.preventDefault();57 const q = query.trim();58 if (!q) return;59 router.push(`/search?q=${encodeURIComponent(q)}`);60 }6162 const isLarge = size === "lg";6364 return (65 <form role="search" onSubmit={onSubmit} className={cn("w-full", className)}>66 <label htmlFor="champ-recherche" className="sr-only">67 Rechercher sur le web québécois68 </label>69 <div className="relative">70 <svg71 aria-hidden="true"72 viewBox="0 0 24 24"73 fill="none"74 stroke="currentColor"75 strokeWidth="2"76 strokeLinecap="round"77 strokeLinejoin="round"78 className={cn(79 "pointer-events-none absolute top-1/2 -translate-y-1/2 text-ink-3",80 isLarge ? "left-5 h-5 w-5" : "left-3.5 h-4 w-4",81 )}82 >83 <circle cx="11" cy="11" r="8" />84 <path d="m21 21-4.35-4.35" />85 </svg>86 <input87 id="champ-recherche"88 type="search"89 name="q"90 value={query}91 onChange={(event) => setQuery(event.target.value)}92 placeholder={placeholder}93 autoComplete="off"94 spellCheck={false}95 // eslint-disable-next-line jsx-a11y/no-autofocus96 autoFocus={autoFocus}97 className={cn(98 "field appearance-none",99 isLarge ? "h-14 pl-12 text-lg" : "h-10 pl-10 text-sm",100 buttonPlacement === "inline"101 ? isLarge102 ? "pr-32"103 : "pr-28"104 : isLarge105 ? "pr-6"106 : "pr-4",107 )}108 />109 {buttonPlacement === "inline" ? (110 <button111 type="submit"112 className={cn(113 "gk-display absolute right-1.5 top-1/2 -translate-y-1/2 rounded-[5px] border-[1.5px] border-ink bg-ink font-bold text-lime transition-colors hover:bg-green-deep active:translate-y-[calc(-50%+1px)]",114 isLarge ? "h-11 px-6 text-base" : "h-7 px-4 text-[13px]",115 )}116 >117 Chercher118 </button>119 ) : null}120 </div>121 {buttonPlacement === "below" ? (122 <button type="submit" className="btn btn-primary mt-8 px-9">123 Chercher124 </button>125 ) : null}126 </form>127 );128}129