Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// pages/Ville.tsx : pages programmatiques SEO — /villes, /ville/:ville[/:type]5// Le serveur rend le même contenu en HTML (louka/seo.py) ; ici, la version6// interactive avec les cartes d'annonces de l'app.7// -----------------------------------------------------------------------------8import { useEffect, useState } from "react";9import { Link, useParams } from "react-router-dom";10import { Listing } from "../api";11import ListingCard from "../components/ListingCard";1213interface VilleRow {14 city: string;15 slug: string;16 n: number;17 avg_price: number | null;18}1920interface VilleData {21 city: string;22 slug: string;23 unit_type: string | null;24 n: number;25 avg: number | null;26 med: number | null;27 types: { unit_type: string; slug: string; n: number }[];28 listings: Listing[];29 neighbors: VilleRow[];30}3132const fmt = (p: number | null) =>33 p == null ? "—" : `${Math.round(p).toLocaleString("fr-CA")} $`;3435async function get<T>(path: string): Promise<T> {36 const res = await fetch(path);37 if (!res.ok) throw new Error(`${res.status}`);38 return res.json();39}4041export function VillesPage() {42 const [villes, setVilles] = useState<VilleRow[] | null>(null);43 const [error, setError] = useState<string | null>(null);4445 useEffect(() => {46 get<{ villes: VilleRow[] }>("/api/seo/villes")47 .then((r) => setVilles(r.villes))48 .catch((e) => setError(String(e)));49 }, []);5051 return (52 <div className="container">53 <span className="kicker">Répertoire — logements par ville</span>54 <h1>Logements à louer par ville</h1>55 <p className="sub">56 Toutes les villes du Québec où Lou-Ka recense des logements à louer,57 avec le nombre d'annonces actives et le loyer moyen.58 </p>59 {error && <div className="notice">⚠️ {error}</div>}60 {!villes && !error && <div className="notice">Chargement…</div>}61 {villes && (62 <ul className="ville-list" style={{ marginTop: 24, lineHeight: 2 }}>63 {villes.map((v) => (64 <li key={v.slug}>65 <Link to={`/ville/${v.slug}`}>{v.city}</Link>66 {" — "}67 {v.n} annonce{v.n > 1 ? "s" : ""}68 {v.avg_price != null && <> · loyer moyen {fmt(v.avg_price)}</>}69 </li>70 ))}71 </ul>72 )}73 </div>74 );75}7677export default function VillePage() {78 const { ville = "", type } = useParams();79 const [data, setData] = useState<VilleData | null>(null);80 const [error, setError] = useState<string | null>(null);8182 useEffect(() => {83 setData(null);84 setError(null);85 const qs = type ? `?type=${encodeURIComponent(type)}` : "";86 get<VilleData>(`/api/seo/ville/${encodeURIComponent(ville)}${qs}`)87 .then(setData)88 .catch((e) =>89 setError(e.message === "404" ? "Ville ou type inconnu." : String(e)));90 window.scrollTo(0, 0);91 }, [ville, type]);9293 if (error)94 return (95 <div className="container">96 <h1>Page introuvable</h1>97 <div className="notice">98 ⚠️ {error} <Link to="/villes">Voir toutes les villes</Link>99 </div>100 </div>101 );102 if (!data)103 return (104 <div className="container">105 <div className="notice">Chargement…</div>106 </div>107 );108109 const what = data.unit_type ? `${data.unit_type} à louer` : "Logements à louer";110 return (111 <div className="container">112 <span className="kicker">113 <Link to="/villes">Villes</Link> — {data.city}114 </span>115 <h1>116 {what} à {data.city}117 </h1>118 <p className="sub">119 {data.n} annonce{data.n > 1 ? "s" : ""} active{data.n > 1 ? "s" : ""}120 {data.avg != null && <> · loyer moyen {fmt(data.avg)}</>}121 {data.med != null && <> · médian {fmt(data.med)}</>}122 </p>123124 {!data.unit_type && data.types.length > 1 && (125 <p style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>126 {data.types.map((t) => (127 <Link key={t.slug} className="pill" to={`/ville/${data.slug}/${t.slug}`}>128 {t.unit_type} ({t.n})129 </Link>130 ))}131 </p>132 )}133 {data.unit_type && (134 <p>135 <Link to={`/ville/${data.slug}`}>136 ← Tous les logements à {data.city}137 </Link>138 </p>139 )}140141 <div className="grid" style={{ marginTop: 24 }}>142 {data.listings.map((l) => (143 <ListingCard key={l.uid} l={l} />144 ))}145 </div>146147 {data.neighbors.length > 0 && (148 <>149 <h2 style={{ marginTop: 40 }}>Autres villes</h2>150 <p style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>151 {data.neighbors.map((v) => (152 <Link key={v.slug} className="pill" to={`/ville/${v.slug}`}>153 {v.city} ({v.n})154 </Link>155 ))}156 </p>157 </>158 )}159 </div>160 );161}162