SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%

refactor(fiche): ordre des sections = ordre du DOM sur tous les breakpoints

Standard Groupe Ka « Ordre des sections — pages détail » (même correctif
que Lou-Ka a6e30ef) : plus de réordonnancement via order:/display:contents
— un bloc ajouté sans order retombait à order:0 et passait devant la
galerie sur mobile.

- Listing.tsx : hero (prix/specs/Vrai-Prix/courtier/CTA) déplacé sous la
  galerie dans la colonne principale ; inclusions + carte forment la
  colonne droite (sticky) sur desktop
- styles.css : suppression des order:/display:contents, colonnes empilées
  en flux naturel sur mobile
- scripts/check-order.mjs : validation Playwright de l ordre visuel

Validé iPhone 14 + desktop 1440px sur 2 fiches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 20, 2026) parent 878916a

3 changed files +121 −76

added frontend/scripts/check-order.mjs +41 −0
@@ -0,0 +1,41 @@
1 +// Validation ordre des sections — fiche Immo-Ka (ordre DOM = ordre visuel)
2 +import { chromium, devices } from "playwright";
3 +
4 +const BASE = process.env.BASE || "http://localhost:18096";
5 +const UID = process.argv[2];
6 +const URL = `${BASE}/propriete/${encodeURIComponent(UID)}`;
7 +const SEL = [".f-galerie", ".f-hero", ".f-desc", "#caracteristiques", "#pieces", "#inclusions", "#carte", ".quartier"];
8 +
9 +async function check(name, ctxOpts) {
10 + const browser = await chromium.launch();
11 + const ctx = await browser.newContext(ctxOpts);
12 + const page = await ctx.newPage();
13 + await page.goto(URL, { waitUntil: "networkidle" });
14 + await page.waitForSelector(".f-galerie", { timeout: 15000 });
15 + await page.waitForTimeout(1200);
16 + const data = await page.evaluate((sel) => {
17 + const out = [];
18 + for (const s of sel) {
19 + const el = document.querySelector(s);
20 + if (!el) { out.push({ s, missing: true }); continue; }
21 + const r = el.getBoundingClientRect();
22 + out.push({ s, hidden: r.height === 0 && r.width === 0, top: Math.round(r.top + window.scrollY), left: Math.round(r.left), order: getComputedStyle(el).order });
23 + }
24 + return { out, scrollY: window.scrollY };
25 + }, SEL);
26 + console.log(`\n=== ${name} === scrollY: ${data.scrollY}`);
27 + for (const b of data.out)
28 + console.log(b.missing ? `${b.s.padEnd(18)} (non rendue)` : b.hidden ? `${b.s.padEnd(18)} (vide/masquée)` :
29 + `${b.s.padEnd(18)} top=${String(b.top).padStart(6)} left=${String(b.left).padStart(4)} order=${b.order}`);
30 + await browser.close();
31 + return data;
32 +}
33 +
34 +const mob = await check("iPhone 14 (mobile)", { ...devices["iPhone 14"] });
35 +await check("Desktop 1440px", { viewport: { width: 1440, height: 900 } });
36 +const vis = mob.out.filter(b => !b.missing && !b.hidden);
37 +const sorted = vis.every((b, i) => i === 0 || b.top >= vis[i - 1].top);
38 +const ok = sorted && mob.scrollY === 0 && vis[0].s === ".f-galerie" && vis.every(b => b.order === "0");
39 +console.log(`\nMOBILE: ordre ${sorted ? "CROISSANT ✓" : "DÉSORDONNÉ ✗"} · scrollY=${mob.scrollY} · 1re=${vis[0].s} · sans order=${vis.every(b => b.order === "0")}`);
40 +console.log(ok ? "VALIDATION OK" : "VALIDATION ÉCHEC");
41 +process.exit(ok ? 0 : 1);
modified frontend/src/pages/Listing.tsx +73 −71
@@ -251,7 +251,8 @@ export default function ListingPage() {
251 251 </nav>
252 252
253 253 <div className="fiche">
254 − {/* -------- colonne gauche : galerie, description, caractéristiques ---- */}
254 + {/* -------- colonne gauche : galerie, prix/synthèse, description, ------
255 + -------- caractéristiques, pièces — ordre du DOM = ordre visuel ---- */}
255 256 <div className="f-col">
256 257 <section className="f-bloc f-galerie" aria-label="Photos">
257 258 <Galerie
@@ -263,76 +264,6 @@ export default function ListingPage() {
263 264 />
264 265 </section>
265 266
266 − {l.description && (
267 − <section className="f-bloc f-desc" id="description">
268 − <h2>Description</h2>
269 − <p className="desc-text">{l.description}</p>
270 − </section>
271 − )}
272 −
273 − {detEntries.length > 0 && (
274 − <section className="f-bloc" id="caracteristiques">
275 − <h2>Caractéristiques</h2>
276 − <div className="dtable">
277 − {detEntries.map(([k, v]) => (
278 − <div className="drow" key={k}>
279 − <span>{k}</span>
280 − {/^https?:\/\//.test(String(v))
281 − ? <b><a href={String(v)} target="_blank" rel="noopener noreferrer">Ouvrir ↗</a></b>
282 − : <b>{String(v)}</b>}
283 − </div>
284 − ))}
285 − </div>
286 − </section>
287 − )}
288 −
289 − {rooms.length > 0 && (
290 − <section className="f-bloc" id="pieces">
291 − <h2>Pièces</h2>
292 − <div className="rooms-wrap">
293 − <table className="rooms">
294 − <thead><tr><th>Pièce</th><th>Niveau</th><th>Dimensions</th><th>Revêtement</th></tr></thead>
295 − <tbody>
296 − {rooms.map((r, i) => (
297 − <tr key={i}>
298 − <td>{r.nom || "—"}</td><td>{r.niveau || "—"}</td>
299 − <td>{r.dimensions || "—"}</td><td>{r.revetement || "—"}</td>
300 − </tr>
301 − ))}
302 − </tbody>
303 − </table>
304 − </div>
305 − </section>
306 − )}
307 −
308 − {l.features && l.features.length > 0 && (
309 − <section className="f-bloc" id="inclusions">
310 − <h2>Inclusions</h2>
311 − <div className="amenity-row">
312 − {l.features.map((f, i) => <span className="amenity" key={i}><Ico name="check" size={13} /> {f}</span>)}
313 − </div>
314 − </section>
315 − )}
316 −
317 − {l.lat != null && l.lng != null && (
318 − <section className="f-bloc" id="carte">
319 − <h2>Emplacement</h2>
320 − <Suspense fallback={<div className="lmap3d lmap3d-skel map-loading">Chargement de la carte…</div>}>
321 − <PropertyMap
322 − uid={l.uid} lat={l.lat} lng={l.lng} price={l.price}
323 − propertyType={l.property_type} address={l.address || l.title}
324 − city={l.city} image={l.images?.[0]}
325 − deal={l.price != null && l.vraiprix?.value != null
326 − && l.price <= l.vraiprix.value * 0.95}
327 − />
328 − </Suspense>
329 − </section>
330 − )}
331 −
332 − </div>
333 −
334 − {/* -------- colonne droite : synthèse, specs, courtier ---------------- */}
335 − <div className="f-col">
336 267 <section className="f-bloc f-hero">
337 268 <div className="price-kicker">
338 269 {l.details?.transaction === "location" ? "Loyer mensuel" : "Prix demandé"}
@@ -445,6 +376,77 @@ export default function ListingPage() {
445 376 Agrégé par Immo-Ka — {sourceName(l.source)}{updated ? ` · synchronisé le ${updated}` : ""}.
446 377 </div>
447 378 </section>
379 +
380 +
381 + {l.description && (
382 + <section className="f-bloc f-desc" id="description">
383 + <h2>Description</h2>
384 + <p className="desc-text">{l.description}</p>
385 + </section>
386 + )}
387 +
388 + {detEntries.length > 0 && (
389 + <section className="f-bloc" id="caracteristiques">
390 + <h2>Caractéristiques</h2>
391 + <div className="dtable">
392 + {detEntries.map(([k, v]) => (
393 + <div className="drow" key={k}>
394 + <span>{k}</span>
395 + {/^https?:\/\//.test(String(v))
396 + ? <b><a href={String(v)} target="_blank" rel="noopener noreferrer">Ouvrir ↗</a></b>
397 + : <b>{String(v)}</b>}
398 + </div>
399 + ))}
400 + </div>
401 + </section>
402 + )}
403 +
404 + {rooms.length > 0 && (
405 + <section className="f-bloc" id="pieces">
406 + <h2>Pièces</h2>
407 + <div className="rooms-wrap">
408 + <table className="rooms">
409 + <thead><tr><th>Pièce</th><th>Niveau</th><th>Dimensions</th><th>Revêtement</th></tr></thead>
410 + <tbody>
411 + {rooms.map((r, i) => (
412 + <tr key={i}>
413 + <td>{r.nom || "—"}</td><td>{r.niveau || "—"}</td>
414 + <td>{r.dimensions || "—"}</td><td>{r.revetement || "—"}</td>
415 + </tr>
416 + ))}
417 + </tbody>
418 + </table>
419 + </div>
420 + </section>
421 + )}
422 +
423 + </div>
424 +
425 + {/* -------- colonne droite (desktop) : inclusions, carte ------------- */}
426 + <div className="f-col">
427 + {l.features && l.features.length > 0 && (
428 + <section className="f-bloc" id="inclusions">
429 + <h2>Inclusions</h2>
430 + <div className="amenity-row">
431 + {l.features.map((f, i) => <span className="amenity" key={i}><Ico name="check" size={13} /> {f}</span>)}
432 + </div>
433 + </section>
434 + )}
435 +
436 + {l.lat != null && l.lng != null && (
437 + <section className="f-bloc" id="carte">
438 + <h2>Emplacement</h2>
439 + <Suspense fallback={<div className="lmap3d lmap3d-skel map-loading">Chargement de la carte…</div>}>
440 + <PropertyMap
441 + uid={l.uid} lat={l.lat} lng={l.lng} price={l.price}
442 + propertyType={l.property_type} address={l.address || l.title}
443 + city={l.city} image={l.images?.[0]}
444 + deal={l.price != null && l.vraiprix?.value != null
445 + && l.price <= l.vraiprix.value * 0.95}
446 + />
447 + </Suspense>
448 + </section>
449 + )}
448 450 </div>
449 451 </div>
450 452
modified frontend/src/styles.css +7 −5
@@ -182,14 +182,16 @@ button { font-family: inherit; }
182 182 .crumbs a { border-bottom: 1.5px solid transparent; }
183 183 .crumbs a:hover { color: var(--green); border-color: var(--green); }
184 184
185 +/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka
186 + « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`
187 + / `display:contents` pour réordonner — un bloc sans `order` retombe à
188 + order:0 et passe devant la galerie sur mobile (bug corrigé sur Lou-Ka).
189 + Mobile : colonnes empilées ; desktop : grid 2 colonnes (fiche | annexes). */
185 190 .fiche { display: flex; flex-direction: column; gap: 22px; }
186 −.f-col { display: contents; }
187 −.f-galerie { order: 1; } .f-hero { order: 2; }
188 −.f-desc { order: 3; } #caracteristiques { order: 4; } #pieces { order: 5; }
189 −#inclusions { order: 6; } #carte { order: 7; }
191 +.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
190 192 @media (min-width: 900px) {
191 193 .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }
192 − .f-col { display: flex; flex-direction: column; gap: 26px; min-width: 0; }
194 + .f-col { gap: 26px; }
193 195 .f-col:last-child { position: sticky; top: 88px; }
194 196 }
195 197 .f-bloc { min-width: 0; }
196 198