Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)
Python 69.3%
TypeScript 16.7%
CSS 7.9%
JavaScript 4.7%
HTML 1.4%
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