SPB Git forge

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

52commits 1branches 0releases
11.6 MBsize
maindefault branch
19 days agolast push
Python 69.3% TypeScript 16.7% CSS 7.9% JavaScript 4.7% HTML 1.4%
4.3 KB · 143 lines tsx
Raw Blame History
1// ==============================================================================2// Author: Simon-Pierre Boucher <contact@spboucher.ai>3// File:   frontend/src/pages/Ville.tsx4// Desc:   Pages programmatiques SEO — /villes (répertoire) et /ville/:ville.5//         Le serveur rend le même contenu en HTML (restoka/seo.py) ; ici, la6//         version interactive avec les cartes de restos de l'app.7// ==============================================================================8import { useEffect, useState } from "react";9import { Link, useParams } from "react-router-dom";10import { fetchRestaurants, Restaurant } from "../api";11import RestoCard from "../components/RestoCard";1213interface VilleRow {14  city: string;15  slug: string;16  n: number;17  with_menu: number;18}1920interface VilleData {21  city: string;22  slug: string;23  n: number;24  with_menu: number;25  neighbors: VilleRow[];26}2728async function getJson<T>(path: string): Promise<T> {29  const res = await fetch(path);30  if (!res.ok) throw new Error(`${res.status}`);31  return res.json();32}3334export function VillesPage() {35  const [villes, setVilles] = useState<VilleRow[] | null>(null);36  const [error, setError] = useState<string | null>(null);3738  useEffect(() => {39    getJson<{ villes: VilleRow[] }>("/api/seo/villes")40      .then((r) => setVilles(r.villes))41      .catch((e) => setError(String(e)));42    window.scrollTo(0, 0);43  }, []);4445  return (46    <div className="container">47      <h1>Restaurants par ville</h1>48      <p className="sub">49        Toutes les villes du Québec où Resto-Ka recense des restaurants, avec50        le nombre d'établissements et de menus complets à prix réels.51      </p>52      {error && <div className="empty">⚠️ {error}</div>}53      {!villes && !error && <div className="empty">Chargement…</div>}54      {villes && (55        <ul style={{ marginTop: 24, lineHeight: 2, paddingLeft: 20 }}>56          {villes.map((v) => (57            <li key={v.slug}>58              <Link to={`/ville/${v.slug}`}>{v.city}</Link>59              {" — "}60              {v.n} établissement{v.n > 1 ? "s" : ""}61              {v.with_menu > 0 && <> · {v.with_menu} menus</>}62            </li>63          ))}64        </ul>65      )}66    </div>67  );68}6970export default function VillePage() {71  const { ville = "" } = useParams();72  const [data, setData] = useState<VilleData | null>(null);73  const [restos, setRestos] = useState<Restaurant[] | null>(null);74  const [error, setError] = useState<string | null>(null);7576  useEffect(() => {77    setData(null);78    setRestos(null);79    setError(null);80    getJson<VilleData>(`/api/seo/ville/${encodeURIComponent(ville)}`)81      .then((d) => {82        setData(d);83        return fetchRestaurants({ city: d.city, limit: 60 }).then((r) =>84          setRestos(r.restaurants));85      })86      .catch((e) =>87        setError(e.message === "404" ? "Ville inconnue." : String(e)));88    window.scrollTo(0, 0);89  }, [ville]);9091  if (error)92    return (93      <div className="container">94        <h1>Page introuvable</h1>95        <div className="empty">96          ⚠️ {error} <Link to="/villes">Voir toutes les villes</Link>97        </div>98      </div>99    );100  if (!data)101    return (102      <div className="container">103        <div className="empty">Chargement…</div>104      </div>105    );106107  return (108    <div className="container">109      <p style={{ margin: "0 0 4px" }}>110        <Link to="/villes">Villes</Link> — {data.city}111      </p>112      <h1>Restaurants à {data.city}</h1>113      <p className="sub">114        {data.n} établissement{data.n > 1 ? "s" : ""}115        {data.with_menu > 0 && (116          <> · {data.with_menu} avec menu complet et prix réels</>117        )}118      </p>119120      <div className="grid" style={{ marginTop: 24 }}>121        {!restos && <div className="empty">Chargement des restos…</div>}122        {restos && restos.map((r) => <RestoCard key={r.uid} r={r} />)}123      </div>124125      {data.neighbors.length > 0 && (126        <>127          <h2 style={{ marginTop: 40 }}>Autres villes</h2>128          <p style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>129            {data.neighbors.map((v) => (130              <Link key={v.slug} to={`/ville/${v.slug}`}>131                {v.city} ({v.n})132              </Link>133            ))}134          </p>135        </>136      )}137      <p style={{ marginTop: 24 }}>138        <Link to="/">← Tous les restaurants du Québec</Link>139      </p>140    </div>141  );142}143