// ----------------------------------------------------------------------------- // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Project: Toit-Ka // pages/Category.tsx : pages programmatiques /louer/{ville}[/{type}], // /acheter/{ville}[/{type}] et /{tx}/type/{type} — mêmes URL que le rendu // SEO serveur (toitka/seo.py), le SPA prend le relais au montage. // ----------------------------------------------------------------------------- import { useEffect, useState } from "react"; import { Link, useParams, useSearchParams } from "react-router-dom"; import { Listing, Tx, fetchListings, fmtPrice, resolveSeo, setDocTitle } from "../api"; import ListingCard from "../components/ListingCard"; import { Ico } from "../components/Icons"; const PAGE_SIZE = 48; // aligné sur toitka/seo.py export default function CategoryPage({ tx }: { tx: Tx }) { const { ville, type } = useParams<{ ville?: string; type?: string }>(); const [params, setParams] = useSearchParams(); const page = Math.max(1, Number(params.get("page")) || 1); const louer = tx === "louer"; const [labels, setLabels] = useState<{ city?: string; type?: string } | null>(null); const [listings, setListings] = useState(null); const [total, setTotal] = useState(0); const [error, setError] = useState(null); useEffect(() => { setLabels(null); setListings(null); setError(null); resolveSeo(tx, ville, type) .then(setLabels) .catch(() => setError("Page introuvable.")); }, [tx, ville, type]); useEffect(() => { if (!labels) return; setListings(null); fetchListings( { tx, city: labels.city, type: labels.type, sort: "price_asc" }, PAGE_SIZE, (page - 1) * PAGE_SIZE) .then((r) => { setListings(r.listings); setTotal(r.total); }) .catch((e) => setError(String(e))); window.scrollTo(0, 0); }, [tx, labels, page]); const verbe = louer ? "à louer" : "à vendre"; const noun = louer ? "Logements" : "Propriétés"; const h1 = labels ? labels.city && labels.type ? `${labels.type} ${verbe} à ${labels.city}` : labels.city ? `${noun} ${verbe} à ${labels.city}` : `${labels.type} ${verbe} au Québec` : ""; useEffect(() => { if (h1) setDocTitle(h1); }, [h1]); if (error) return (

Page introuvable

{error}

Toutes les annonces
); const pages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const refine = new URLSearchParams({ tx }); if (labels?.city) refine.set("city", labels.city); if (labels?.type) refine.set("type", labels.type); return (

{h1 || "Chargement…"}

{listings && (

{total.toLocaleString("fr-CA")} annonces {listings.length > 0 && listings[0].price != null && ( <> · à partir de {fmtPrice(listings[0].price, tx)} )} {" · "} Affiner la recherche (carte, filtres)

)} {!listings && !error && (
{Array.from({ length: 8 }).map((_, i) => (
))}
)} {listings && (
{listings.map((l) => )}
)} {pages > 1 && ( )}
); }