/**
* Trouve-KA — contenu client de la page de résultats
* Author: Simon-Pierre Boucher
* Contact: contact@spboucher.ai
*/
"use client";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { FilterPills, type SearchFilters } from "@/components/filter-pills";
import { GallerySkeleton, ImageGallery } from "@/components/image-gallery";
import { ResultItem } from "@/components/result-item";
import { SearchBar } from "@/components/search-bar";
import { Button } from "@/components/ui/button";
import { formatNumber } from "@/lib/format";
import type { SearchResponse } from "@/lib/types";
const PAGE_SIZE = 10;
const GALLERY_PAGE_SIZE = 24;
function buildSearchUrl(
q: string,
page: number,
filters: SearchFilters,
): string {
const params = new URLSearchParams({ q });
if (page > 1) params.set("page", String(page));
if (filters.category) params.set("category", filters.category);
if (filters.language) params.set("language", filters.language);
if (filters.quebecOnly) params.set("quebec_only", "true");
if (filters.images) params.set("images", "true");
return `/search?${params.toString()}`;
}
function ResultsSkeleton() {
return (
{Array.from({ length: 5 }, (_, i) => (
{i % 2 === 0 ? (
) : null}
))}
);
}
export function SearchContent() {
const router = useRouter();
const searchParams = useSearchParams();
const q = searchParams.get("q")?.trim() ?? "";
const page = Math.max(1, Number.parseInt(searchParams.get("page") ?? "1", 10) || 1);
const filters: SearchFilters = {
category: searchParams.get("category") ?? undefined,
language: searchParams.get("language") ?? undefined,
quebecOnly: searchParams.get("quebec_only") === "true",
images: searchParams.get("images") === "true",
};
const imagesMode = Boolean(filters.images);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(Boolean(q));
const [error, setError] = useState(false);
useEffect(() => {
if (!q) {
setData(null);
setLoading(false);
return;
}
const controller = new AbortController();
setLoading(true);
setError(false);
const gallery = searchParams.get("images") === "true";
const params = new URLSearchParams({
q,
page: String(page),
limit: String(gallery ? GALLERY_PAGE_SIZE : PAGE_SIZE),
});
const category = searchParams.get("category");
const language = searchParams.get("language");
if (category) params.set("category", category);
if (language) params.set("language", language);
if (searchParams.get("quebec_only") === "true") {
params.set("quebec_only", "true");
}
if (gallery) params.set("images", "true");
fetch(`/api/search?${params.toString()}`, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`Erreur ${res.status}`);
return res.json() as Promise;
})
.then((json) => {
setData(json);
setLoading(false);
})
.catch((err: unknown) => {
if (err instanceof DOMException && err.name === "AbortError") return;
setError(true);
setLoading(false);
});
return () => controller.abort();
}, [q, page, searchParams]);
function navigate(nextPage: number, nextFilters: SearchFilters) {
router.push(buildSearchUrl(q, nextPage, nextFilters));
window.scrollTo({ top: 0 });
}
const totalPages = data ? Math.max(1, Math.ceil(data.total / data.limit)) : 1;
const hasPrevious = page > 1;
const hasNext = data ? page < totalPages : false;
return (
{/* Barre persistante sous l'en-tête global (h-14) : recherche + filtres. */}
{!q ? (
Entrez une requête pour chercher le web québécois.
) : loading ? (
imagesMode ? :
) : error ? (
Le moteur est momentanément injoignable.
Réessayez dans quelques instants ou consultez l'
état du moteur
.
) : data && data.results.length === 0 ? (
0 résultat
Rien pour « {q} » — encore.
Vérifiez l'orthographe, essayez des termes plus généraux ou
retirez un filtre. Le robot crawle le Québec en continu : ce
qui manque aujourd'hui peut apparaître demain.
Soumettre un site québécois
) : data ? (
<>
{formatNumber(data.total)}
{" "}
{data.total === 1 ? "résultat" : "résultats"} (
{formatNumber(data.took_ms)} ms)
{imagesMode ? (
) : (
{data.results.map((result) => (
))}
)}
{hasPrevious || hasNext ? (
navigate(page - 1, filters)}
>
← Précédent
Page {formatNumber(page)} de {formatNumber(totalPages)}
navigate(page + 1, filters)}
>
Suivant →
) : null}
>
) : null}
);
}