// ============================================================================== // Author: Simon-Pierre Boucher // File: pages/Home.tsx // Desc: Accueil — héros + bascule RESTOS / PLATS (recherche par plat dans // tous les menus), chips cuisines, barre de filtres (région, ville, // type, diète, prix), grilles de résultats + pagination numérotée. // Filtres synchronisés à l'URL, feuille mobile, skeletons (patron // Lou·Ka, design éditorial sharp Groupe KA). // ============================================================================== import { useEffect, useMemo, useState } from "react"; import { Link, useSearchParams } from "react-router-dom"; import { CONTEXT_SHORT, Dish, Facets, Restaurant, cuisineLabel, dietLabel, fetchDishes, fetchFacets, fetchRecentRestaurants, fetchRestaurants, fetchStats, fmtPrice, getRecentUids, typeLabel, } from "../api"; import { IcoBag, IcoLeaf, IcoPin, IcoPlate, IcoSearch, IcoTag } from "../components/Icons"; import Pager from "../components/Pager"; import RestoCard from "../components/RestoCard"; const PAGE_SIZE = 24; const DISH_PAGE_SIZE = 20; const FILTER_KEYS = ["q", "region", "city", "cuisine", "establishment_type", "diet", "price_range", "sort", "mode", "has_menu"] as const; type FilterKey = typeof FILTER_KEYS[number]; function DishRow({ d }: { d: Dish }) { return ( {d.image ? ( {d.item} ) : ( )}
{d.item}
{d.description &&

{d.description}

}
{d.brand} {d.section} {d.locations > 1 ? {d.locations} emplacements : {d.city}{d.region ? ` · ${d.region}` : ""}} {CONTEXT_SHORT[d.price_context] ?? d.price_context}
); } export default function Home() { const [params, setParams] = useSearchParams(); const [facets, setFacets] = useState(null); const [restos, setRestos] = useState([]); const [dishes, setDishes] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [page, setPage] = useState(0); const [statChips, setStatChips] = useState<{ label: string; value: number }[]>([]); const [sheetOpen, setSheetOpen] = useState(false); const [recent, setRecent] = useState([]); useEffect(() => { fetchRecentRestaurants(getRecentUids().slice(0, 8)) .then(setRecent) .catch(() => {}); }, []); // feuille de filtres ouverte : gel du scroll d'arrière-plan + fermeture à // Escape (même pattern que le menu hamburger d'App.tsx) useEffect(() => { document.body.style.overflow = sheetOpen ? "hidden" : ""; if (!sheetOpen) return () => { document.body.style.overflow = ""; }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setSheetOpen(false); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = ""; window.removeEventListener("keydown", onKey); }; }, [sheetOpen]); const filters = useMemo(() => { const f: Partial> = {}; for (const k of FILTER_KEYS) { const v = params.get(k); if (v) f[k] = v; } return f; }, [params]); const mode = filters.mode === "plats" ? "plats" : "restos"; const pageSize = mode === "plats" ? DISH_PAGE_SIZE : PAGE_SIZE; const setFilter = (key: FilterKey, value: string) => { const next = new URLSearchParams(params); if (value) next.set(key, value); else next.delete(key); setParams(next, { replace: true }); setPage(0); }; useEffect(() => { fetchStats() .then((s) => setStatChips([ { label: "restaurants", value: s.restaurants }, { label: "avec menu complet", value: s.with_menu }, { label: "plats avec prix", value: s.items }, { label: "régions couvertes", value: s.regions }, ])) .catch(() => {}); }, []); useEffect(() => { fetchFacets(filters.region).then(setFacets).catch(() => {}); }, [filters.region]); useEffect(() => { setLoading(true); setError(false); if (mode === "plats") { const q = (filters.q ?? "").trim(); if (q.length < 2) { setDishes([]); setTotal(0); setLoading(false); return; } fetchDishes({ q, region: filters.region, city: filters.city, cuisine: filters.cuisine, limit: DISH_PAGE_SIZE, offset: page * DISH_PAGE_SIZE, }) .then((r) => { setDishes(r.dishes); setTotal(r.total); }) .catch(() => setError(true)) .finally(() => setLoading(false)); } else { const { mode: _m, ...rest } = filters; fetchRestaurants({ ...rest, limit: PAGE_SIZE, offset: page * PAGE_SIZE }) .then((r) => { setRestos(r.restaurants); setTotal(r.total); }) .catch(() => setError(true)) .finally(() => setLoading(false)); } }, [filters, page, mode]); const activePills: { key: FilterKey; label: string }[] = []; if (filters.region) activePills.push({ key: "region", label: filters.region }); if (filters.city) activePills.push({ key: "city", label: filters.city }); if (filters.cuisine) activePills.push({ key: "cuisine", label: cuisineLabel(filters.cuisine) }); if (filters.establishment_type) activePills.push({ key: "establishment_type", label: typeLabel(filters.establishment_type) }); if (filters.diet) activePills.push({ key: "diet", label: dietLabel(filters.diet) }); if (filters.price_range) activePills.push({ key: "price_range", label: filters.price_range }); if (filters.has_menu === "1") activePills.push({ key: "has_menu", label: "Avec menu complet" }); if (filters.has_menu === "0") activePills.push({ key: "has_menu", label: "Fiche seulement" }); if (filters.q) activePills.push({ key: "q", label: `« ${filters.q} »` }); const clearAll = () => { setParams(new URLSearchParams(), { replace: true }); setPage(0); }; const filterbar = (
Filtres
{mode === "restos" && ( <> )}
); return (
Agrégateur — 17 régions du Québec

Tous les restos. Tous les menus.
Tous les prix.

Resto-Ka rassemble les restaurants du Québec avec leurs menus complets et leurs prix réels — cherchez un resto ou un plat, comparez, le contexte du prix (salle, emporter, livraison) est toujours affiché.

{statChips.length > 0 && (
En direct — signes vitaux
{statChips.map((s, i) => (
{String(i + 1).padStart(2, "0")} {s.value.toLocaleString("fr-CA")} {s.label}
))}
)}
Prix réels, jamais sans contexte
17 régions du Québec
Recherche par plat
Filtres diète & cuisine
{/* chips cuisines (défilement horizontal) */}
{(facets?.cuisines ?? []).slice(0, 14).map((c) => ( ))}
{filterbar} {sheetOpen &&
setSheetOpen(false)} aria-hidden="true" />} {activePills.length > 0 && (
{activePills.map((p) => ( ))}
)} {page === 0 && recent.length > 0 && (

Récemment consultés

vos dernières fiches
{recent.map((r) => )}
)}

{mode === "plats" ? "Plats" : "Restaurants"}

{mode === "plats" && (filters.q ?? "").trim().length < 2 ? "tapez un plat (min. 2 lettres)" : `${total.toLocaleString("fr-CA")} résultat${total > 1 ? "s" : ""}`}
{error && (

Impossible de charger les résultats

Réessayez dans un instant.

)} {loading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : mode === "plats" ? ( (filters.q ?? "").trim().length < 2 ? (

Cherchez n'importe quel plat

« Poutine », « tartare de saumon », « pad thaï »… tous les menus du Québec sont indexés, avec leurs prix réels.

) : dishes.length === 0 ? (

Aucun plat trouvé

Essayez un autre mot — la couverture grandit chaque semaine.

) : ( <>
{dishes.map((d, i) => )}
) ) : restos.length === 0 && !error ? (

Aucun resto ne correspond

Élargissez les filtres — la couverture grandit chaque semaine.

) : ( <>
{restos.map((r) => )}
)}
); }