spb/toit-ka Public
Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com
Python 40.2%
TypeScript 39%
CSS 20.2%
HTML 0.7%
1// -----------------------------------------------------------------------------2// Author: Simon-Pierre Boucher3// Contact: contact@spboucher.ai4// Project: Toit-Ka5// pages/Category.tsx : pages programmatiques /louer/{ville}[/{type}],6// /acheter/{ville}[/{type}] et /{tx}/type/{type} — mêmes URL que le rendu7// SEO serveur (toitka/seo.py), le SPA prend le relais au montage.8// -----------------------------------------------------------------------------9import { useEffect, useState } from "react";10import { Link, useParams, useSearchParams } from "react-router-dom";11import { Listing, Tx, fetchListings, fmtPrice, resolveSeo, setDocTitle } from "../api";12import ListingCard from "../components/ListingCard";13import { Ico } from "../components/Icons";1415const PAGE_SIZE = 48; // aligné sur toitka/seo.py1617export default function CategoryPage({ tx }: { tx: Tx }) {18 const { ville, type } = useParams<{ ville?: string; type?: string }>();19 const [params, setParams] = useSearchParams();20 const page = Math.max(1, Number(params.get("page")) || 1);21 const louer = tx === "louer";2223 const [labels, setLabels] = useState<{ city?: string; type?: string } | null>(null);24 const [listings, setListings] = useState<Listing[] | null>(null);25 const [total, setTotal] = useState(0);26 const [error, setError] = useState<string | null>(null);2728 useEffect(() => {29 setLabels(null); setListings(null); setError(null);30 resolveSeo(tx, ville, type)31 .then(setLabels)32 .catch(() => setError("Page introuvable."));33 }, [tx, ville, type]);3435 useEffect(() => {36 if (!labels) return;37 setListings(null);38 fetchListings(39 { tx, city: labels.city, type: labels.type, sort: "price_asc" },40 PAGE_SIZE, (page - 1) * PAGE_SIZE)41 .then((r) => { setListings(r.listings); setTotal(r.total); })42 .catch((e) => setError(String(e)));43 window.scrollTo(0, 0);44 }, [tx, labels, page]);4546 const verbe = louer ? "à louer" : "à vendre";47 const noun = louer ? "Logements" : "Propriétés";48 const h1 = labels49 ? labels.city && labels.type50 ? `${labels.type} ${verbe} à ${labels.city}`51 : labels.city52 ? `${noun} ${verbe} à ${labels.city}`53 : `${labels.type} ${verbe} au Québec`54 : "";55 useEffect(() => { if (h1) setDocTitle(h1); }, [h1]);5657 if (error)58 return (59 <div className="notice container">60 <div className="big"><Ico name="alert" size={44} /></div>61 <h2>Page introuvable</h2>62 <p>{error}</p>63 <Link className="btn btn-primary" to="/">Toutes les annonces</Link>64 </div>65 );6667 const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));68 const refine = new URLSearchParams({ tx });69 if (labels?.city) refine.set("city", labels.city);70 if (labels?.type) refine.set("type", labels.type);7172 return (73 <div className="container">74 <nav className="crumbs" aria-label="Fil d'Ariane" style={{ margin: "14px 0 4px" }}>75 <Link to={`/?tx=${tx}`}>Accueil</Link>76 {labels?.city && ville && <> › <Link to={`/${tx}/${ville}`}>{verbe.charAt(0).toUpperCase() + verbe.slice(1)} à {labels.city}</Link></>}77 {labels?.type && <> › <span>{labels.type}</span></>}78 </nav>79 <h1>{h1 || "Chargement…"}</h1>80 {listings && (81 <p style={{ opacity: 0.75, margin: "6px 0 14px" }}>82 <b>{total.toLocaleString("fr-CA")}</b> annonces83 {listings.length > 0 && listings[0].price != null && (84 <> · à partir de <b>{fmtPrice(listings[0].price, tx)}</b></>85 )}86 {" · "}87 <Link to={`/?${refine}`}>Affiner la recherche (carte, filtres)</Link>88 </p>89 )}90 {!listings && !error && (91 <div className="grid" aria-busy="true">92 {Array.from({ length: 8 }).map((_, i) => (93 <div key={i} className="card skel"><div className="sk-img" /><div className="sk-line" /></div>94 ))}95 </div>96 )}97 {listings && (98 <div className="grid">99 {listings.map((l) => <ListingCard key={l.uid} l={l} />)}100 </div>101 )}102 {pages > 1 && (103 <nav className="pager" aria-label="Pagination" style={{ margin: "18px 0" }}>104 {page > 1 && (105 <button className="btn" onClick={() => setParams(page === 2 ? {} : { page: String(page - 1) })}>106 ← Précédente107 </button>108 )}109 <span style={{ margin: "0 10px" }}>Page {page} de {pages}</span>110 {page < pages && (111 <button className="btn" onClick={() => setParams({ page: String(page + 1) })}>112 Suivante →113 </button>114 )}115 </nav>116 )}117 </div>118 );119}120