SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.0 KB · 162 lines tsx
Raw Blame History
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