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%

Fiche logement : refonte editoriale v2 — icones contextuelles, sections sans boites

- Nouveau composant AmenityIco (~34 pictos maison, trait 1.8) : chaque
  inclusion/caracteristique est appariee par mots-cles (accents neutralises)
  a SON icone — lave-vaisselle, chauffage, balcon, stationnement, animaux,
  piscine, foyer… — fini le meme ✓ repete dix fois
- Inclusions : grille en filets (tuile icone 32px accent-soft, libelle,
  ✓ discret a droite pour le confirme ; neutre pour les mentions libres)
- Chips cles du hero : hairline + icone contextuelle (3½ → porte, dispo →
  calendrier, pi² → regle…) au lieu d aplats marine
- Colonne editoriale (gauche) de-boxee : description, inclusions, details
  pratiques respirent sans carte — les modules de donnees (droite) restent
  en cartes ; titres avec tiret accent net (sans bordure/ombre heritees)
- Details pratiques : rangees en filets, cles en micro-mono, fini les tuiles
- Prix 38-54px, ancres en onglets textes, en-bref en filet accent,
  galerie au cadre allege (hairline, rayon 10)
- Mobile : le CTA sticky prend le bas d ecran, la tabbar s efface sur la
  fiche (body:has(.cta-sticky)) — plus de chevauchement
Simon-Pierre Boucher committed 1 mo ago (Aug 25, 2026) parent a79d1d5

3 changed files +192 −4

added frontend/src/components/AmenityIco.tsx +112 −0
@@ -0,0 +1,112 @@
1 +// -----------------------------------------------------------------------------
2 +// Groupe KA — composant partagé (lou-ka / immo-ka)
3 +// components/AmenityIco.tsx : icône contextuelle d'une commodité ou inclusion.
4 +// Fini l'icône unique répétée dix fois : chaque libellé est associé par
5 +// mots-clés (accents neutralisés) à l'un des ~30 pictos maison — trait 1.8,
6 +// 24×24, currentColor. Repli : ✓ (confirmé) ou étoile (mention libre).
7 +// -----------------------------------------------------------------------------
8 +import { ReactNode } from "react";
9 +
10 +const ICONS: Record<string, ReactNode> = {
11 + heat: (<><rect x="4" y="9" width="16" height="9" rx="2" /><path d="M8 9v9M12 9v9M16 9v9M7.6 3.5c0 1.6 1.2 1.6 1.2 3.2M11.4 3.5c0 1.6 1.2 1.6 1.2 3.2M15.2 3.5c0 1.6 1.2 1.6 1.2 3.2" /></>),
12 + bolt: <path d="M13 2.5 4.5 13.5H11l-1.5 8 8.5-11H11.5l1.5-8z" />,
13 + droplet: (<><path d="M12 3.5S6.2 9.8 6.2 13.6a5.8 5.8 0 0 0 11.6 0C17.8 9.8 12 3.5 12 3.5z" /><path d="M9.4 14a2.7 2.7 0 0 0 2.1 2.6" /></>),
14 + wifi: <path d="M3.5 9.5a13 13 0 0 1 17 0M6.5 13a8.5 8.5 0 0 1 11 0M9.5 16.4a4.4 4.4 0 0 1 5 0M12 19.6h.01" />,
15 + dishwasher: (<><rect x="4.5" y="3" width="15" height="18" rx="2" /><path d="M4.5 8.5h15M8 12.5V17M12 12.5V17M16 12.5V17M7.5 5.7h.01M10.5 5.7h.01" /></>),
16 + washer: (<><rect x="4.5" y="3" width="15" height="18" rx="2" /><circle cx="12" cy="14" r="4.2" /><path d="M4.5 7.5h15M16.4 5.2h.01M9.2 14c1 .8 1.9.8 2.8 0s1.9-.8 2.8 0" /></>),
17 + fridge: (<><rect x="6.5" y="2.5" width="11" height="19" rx="2" /><path d="M6.5 9.5h11M9.5 5.5v2M9.5 12.5V16" /></>),
18 + snow: <path d="M12 2.5v19M3.8 7.25l16.4 9.5M20.2 7.25 3.8 16.75" />,
19 + elevator: (<><rect x="4.5" y="3" width="15" height="18" rx="2" /><path d="M12 3v18M7 11l1.5-1.8L10 11M14 13l1.5 1.8L17 13" /></>),
20 + balcony: <path d="M4 11.5h16M5 11.5V20M19 11.5V20M9.5 11.5V20M14.5 11.5V20M4 20h16M7 11.5V6.5a5 5 0 0 1 10 0v5" />,
21 + pool: <path d="M9 4.5v9M13.5 4.5v9M9 7.5h4.5M3 16.5c1.5 1.3 3 1.3 4.5 0s3-1.3 4.5 0 3 1.3 4.5 0 3-1.3 4.5 0M3 20c1.5 1.3 3 1.3 4.5 0s3-1.3 4.5 0 3 1.3 4.5 0 3-1.3 4.5 0" />,
22 + gym: <path d="M6.7 6.7v10.6M17.3 6.7v10.6M3.5 9.2v5.6M20.5 9.2v5.6M6.7 12h10.6" />,
23 + laundry: <path d="M4 9.5h16l-1.8 10a1.8 1.8 0 0 1-1.8 1.5H7.6a1.8 1.8 0 0 1-1.8-1.5zM8 9.5 12 3l4 6.5M9.3 13.5v3.5M12 13.5v3.5M14.7 13.5v3.5" />,
24 + box: <path d="M3.5 8 12 3.5 20.5 8v8L12 20.5 3.5 16zM3.5 8 12 12.5 20.5 8M12 12.5v8" />,
25 + parking: (<><rect x="3.5" y="3.5" width="17" height="17" rx="3.5" /><path d="M9.5 16.5v-9H13a2.9 2.9 0 0 1 0 5.8H9.5" /></>),
26 + garage: <path d="M3.5 20V9.5L12 4l8.5 5.5V20M7 20v-6.5h10V20M7 16.8h10" />,
27 + sofa: <path d="M5.5 11V8.8A2.8 2.8 0 0 1 8.3 6h7.4a2.8 2.8 0 0 1 2.8 2.8V11M3.5 13.7a2.2 2.2 0 0 1 4.4 0v1h8.2v-1a2.2 2.2 0 0 1 4.4 0V17.5h-17zM5.5 17.5v1.8M18.5 17.5v1.8" />,
28 + nosmoke: <path d="M4 4l16 16M14.5 13H4v3h6.5M17.5 13H20v3h-1" />,
29 + paw: (<><circle cx="8.2" cy="7.8" r="1.7" /><circle cx="15.8" cy="7.8" r="1.7" /><circle cx="4.8" cy="12" r="1.6" /><circle cx="19.2" cy="12" r="1.6" /><path d="M12 11c2.8 0 5.2 2.3 5.2 4.8 0 1.8-1.4 3-3.2 3-.8 0-1.3-.3-2-.3s-1.2.3-2 .3c-1.8 0-3.2-1.2-3.2-3C6.8 13.3 9.2 11 12 11z" /></>),
30 + fire: (<><path d="M4 4.5h16M5.5 4.5V19.5M18.5 4.5V19.5M4 19.5h16" /><path d="M12 8.5c-1.8 2-3 3.4-3 5.3a3 3 0 0 0 6 0c0-1.9-1.2-3.3-3-5.3z" /></>),
31 + tree: <path d="M12 3 7.2 9.5h2.4L5.5 15.5h13L14.4 9.5h2.4zM12 15.5V21" />,
32 + spa: <path d="M8 3.5c0 1.7-1.4 2-1.4 3.7S8 9.5 8 11M12.5 3.5c0 1.7-1.4 2-1.4 3.7s1.4 2.3 1.4 3.8M17 3.5c0 1.7-1.4 2-1.4 3.7S17 9.5 17 11M3.5 15.5c1.4 1.3 2.8 1.3 4.2 0s2.9-1.3 4.3 0 2.8 1.3 4.2 0 2.9-1.3 4.3 0M3.5 19.5c1.4 1.3 2.8 1.3 4.2 0s2.9-1.3 4.3 0 2.8 1.3 4.2 0 2.9-1.3 4.3 0" />,
33 + shield: (<><path d="M12 3 5 6v5.5c0 4.4 3 7.6 7 9.5 4-1.9 7-5.1 7-9.5V6z" /><path d="m9 11.5 2.2 2.2L15.5 9" /></>),
34 + eye: (<><path d="M2.5 12S6.3 5.8 12 5.8 21.5 12 21.5 12 17.7 18.2 12 18.2 2.5 12 2.5 12z" /><circle cx="12" cy="12" r="2.8" /></>),
35 + waves: <path d="M3 9c1.5 1.3 3 1.3 4.5 0s3-1.3 4.5 0 3 1.3 4.5 0 3-1.3 4.5 0M3 14c1.5 1.3 3 1.3 4.5 0s3-1.3 4.5 0 3 1.3 4.5 0 3-1.3 4.5 0M3 19c1.5 1.3 3 1.3 4.5 0s3-1.3 4.5 0 3 1.3 4.5 0 3-1.3 4.5 0" />,
36 + calendar: (<><rect x="4" y="5" width="16" height="16" rx="2" /><path d="M4 10h16M8.5 3v4M15.5 3v4M8 14h.01M12 14h.01M16 14h.01" /></>),
37 + ruler: <path d="M3.5 16.2 16.2 3.5l4.3 4.3L7.8 20.5zM7.8 15.9l1.8 1.8M10.6 13.1l1.8 1.8M13.4 10.3l1.8 1.8M16.2 7.5 18 9.3" />,
38 + bed: <path d="M3.5 18V6.5M3.5 13.5h17V18M3.5 10h6.5v3.5M20.5 13.5v-1a3 3 0 0 0-3-3H10" />,
39 + bath: <path d="M4 12.5h16v1.7a4.8 4.8 0 0 1-4.8 4.8H8.8A4.8 4.8 0 0 1 4 14.2zM6 12.5V6a2.5 2.5 0 0 1 4.6-1.3M7 19l-1 1.8M17 19l1 1.8" />,
40 + people: (<><circle cx="9" cy="8" r="3.2" /><path d="M3.5 20.5c0-3.1 2.4-5.2 5.5-5.2s5.5 2.1 5.5 5.2M15.5 5.4a3.2 3.2 0 0 1 0 5.2M20.5 20.5c0-2.7-1.8-4.6-4.3-5.1" /></>),
41 + stairs: <path d="M3.5 20.5h4.2v-4.2h4.2v-4.2h4.2V7.9h4.4" />,
42 + door: (<><rect x="6" y="3" width="12" height="18" rx="1.5" /><path d="M14.8 12h.01M4 21h16" /></>),
43 + home: <path d="m3.5 11 8.5-7 8.5 7M6 9.5V20h12V9.5" />,
44 + sun: (<><circle cx="12" cy="12" r="4" /><path d="M12 2.5v2.5M12 19v2.5M2.5 12H5M19 12h2.5M5.3 5.3l1.8 1.8M16.9 16.9l1.8 1.8M18.7 5.3l-1.8 1.8M7.1 16.9l-1.8 1.8" /></>),
45 + check: <path d="m4.5 12.5 5.3 5.3L19.5 6.5" />,
46 + spark: <path d="M12 3.5 13.9 10l6.6 2-6.6 2L12 20.5 10.1 14l-6.6-2 6.6-2z" />,
47 +};
48 +
49 +/** [motif (sur libellé minuscule sans accents), clé d'icône] — ordre = priorité */
50 +const RULES: [RegExp, string][] = [
51 + [/lave-?vaisselle/, "dishwasher"],
52 + [/laveuse|secheuse|lessive/, "washer"],
53 + [/buanderie/, "laundry"],
54 + [/electromenager|frigo|refrigerateur|cuisiniere|four incl|poele/, "fridge"],
55 + [/climatis|air clim|thermopompe|echangeur d.air|\ba\/?c\b/, "snow"],
56 + [/chauff/, "heat"],
57 + [/eau chaude/, "droplet"],
58 + [/electric|eclair|hydro/, "bolt"],
59 + [/internet|wi-?fi|cablodistribution|\bcable\b|fibre/, "wifi"],
60 + [/ascenseur/, "elevator"],
61 + [/balcon|terrasse|patio|loggia|veranda/, "balcony"],
62 + [/piscine/, "pool"],
63 + [/gym|salle d.entrainement|exercice/, "gym"],
64 + [/rangement|locker|entreposage|walk-?in|penderie/, "box"],
65 + [/garage/, "garage"],
66 + [/stationnement|parking|abri d.auto/, "parking"],
67 + [/meuble/, "sofa"],
68 + [/non-?fumeur|sans fumee/, "nosmoke"],
69 + [/animau|chat|chien|\bpet\b/, "paw"],
70 + [/foyer|cheminee|poele a bois/, "fire"],
71 + [/spa\b|jacuzzi|sauna|bain tourbillon/, "spa"],
72 + [/securite|surveill|camera|alarme|interphone|concierge|portier/, "shield"],
73 + [/\bvue\b|panoram/, "eye"],
74 + [/bord de l.eau|acces au lac|\blac\b|riviere|plage|navigable/, "waves"],
75 + [/cour|jardin|arbre|boise|verdure|gazon|amenagement paysager/, "tree"],
76 + [/ensoleill|luminosite|lumineux|solarium/, "sun"],
77 + [/dispo|libre |libre$/, "calendar"],
78 + [/pi2|pieds carres|superficie|\bm2\b/, "ruler"],
79 + [/chambre/, "bed"],
80 + [/salle[s]? de bain|salle[s]? d.eau|douche|\bsdb\b/, "bath"],
81 + [/occupant|personne|colocataire/, "people"],
82 + [/etage|niveau|escalier|mezzanine/, "stairs"],
83 + [/studio|loft|(^|\s)\d ?1\/2|½/, "door"],
84 + [/maison|plain-?pied|unifamiliale/, "home"],
85 + [/egout|septique|fosse/, "waves"],
86 + [/aqueduc|approvisionnement en eau|puits/, "droplet"],
87 + [/construction|neuve|fondation|toiture|revetement|renov/, "home"],
88 + [/commerce|zonage|usage/, "box"],
89 +];
90 +
91 +/** minuscules + accents neutralisés, pour un appariement robuste */
92 +const norm = (s: string) =>
93 + s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
94 +
95 +export function amenityKey(label: string, fallback = "check"): string {
96 + const n = norm(label);
97 + for (const [re, key] of RULES) if (re.test(n)) return key;
98 + return fallback;
99 +}
100 +
101 +export default function AmenityIco({ label, size = 16, fallback = "check" }:
102 + { label: string; size?: number; fallback?: string }) {
103 + return (
104 + <svg
105 + width={size} height={size} viewBox="0 0 24 24" aria-hidden="true"
106 + fill="none" stroke="currentColor" strokeWidth={1.8}
107 + strokeLinecap="round" strokeLinejoin="round"
108 + >
109 + {ICONS[amenityKey(label, fallback)] ?? ICONS.check}
110 + </svg>
111 + );
112 +}
modified frontend/src/pages/Listing.tsx +16 −4
@@ -30,6 +30,7 @@ import ImmeubleBloc from "../components/ImmeubleBloc";
30 30 import GestionnaireBloc from "../components/GestionnaireBloc";
31 31 import HiverScore from "../components/HiverScore";
32 32 import { IcoAlert, IcoDoc } from "../components/Icons";
33 +import AmenityIco from "../components/AmenityIco";
33 34 import KaScoresBlock from "../components/KaScoresBlock";
34 35 import { markSeen } from "../search/seen";
35 36
@@ -378,7 +379,11 @@ export default function ListingPage() {
378 379 {[l.address !== l.title ? l.address : "", l.sector, l.city].filter(Boolean).join(" · ")}
379 380 </div>
380 381 <div className="chips-scroll" role="list" aria-label="Caractéristiques clés">
381 − {chips.map((c) => <span className="chip-key" role="listitem" key={c}>{c}</span>)}
382 + {chips.map((c) => (
383 + <span className="chip-key" role="listitem" key={c}>
384 + <AmenityIco label={c} size={14} fallback="spark" /> {c}
385 + </span>
386 + ))}
382 387 </div>
383 388 <nav className="ancres" aria-label="Sections de la fiche">
384 389 <a href="#description">Description</a>
@@ -424,12 +429,19 @@ export default function ListingPage() {
424 429 <section className="f-bloc f-incl" id="inclusions">
425 430 <h2>Inclusions et commodités</h2>
426 431 {zeroFrais && <div className="deal-badge deal-good">💡 Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais cachés</div>}
427 − <div className="amenity-row">
432 + <div className="amenity-grid">
428 433 {confirmes.map((b) => (
429 − <span className="amenity confirmed" key={`c-${b}`}>✓ {b}</span>
434 + <span className="amenity-it confirmed" key={`c-${b}`}>
435 + <span className="am-ico"><AmenityIco label={b} /></span>
436 + <span className="am-txt">{b}</span>
437 + <span className="am-conf" title="Confirmé par les données structurées de la source">✓</span>
438 + </span>
430 439 ))}
431 440 {autres.map((a) => (
432 − <span className="amenity unconfirmed" key={a} title="Mentionné par la source, sans confirmation structurée">{a}</span>
441 + <span className="amenity-it unconfirmed" key={a} title="Mentionné par la source, sans confirmation structurée">
442 + <span className="am-ico"><AmenityIco label={a} fallback="spark" /></span>
443 + <span className="am-txt">{a}</span>
444 + </span>
433 445 ))}
434 446 </div>
435 447 {confirmes.length === 0 && autres.length === 0 && (
modified frontend/src/styles.css +64 −0
@@ -2711,3 +2711,67 @@ html { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */
2711 2711 }
2712 2712 /* le badge Groupe KA lâche prise un peu plus tôt (débordement de ~6px à 1100) */
2713 2713 @media (max-width: 1140px) { .header .gk-badge { display: none; } }
2714 +
2715 +/* ---- Fiche logement : DA v2 — sections en filets, icônes contextuelles ---- */
2716 +.f-bloc > h2 { display: flex; align-items: center; gap: 10px; font-size: 21px; }
2717 +.f-bloc > h2::before { content: ""; width: 20px; height: 3px; border-radius: 2px; background: var(--accent); flex: none; }
2718 +.f-hero .price { font-size: clamp(38px, 8.5vw, 54px); letter-spacing: -0.04em; }
2719 +.f-hero h1 { color: var(--ink); }
2720 +.f-hero .loc { font-size: 14px; }
2721 +/* chips clés : hairline + icône contextuelle (plus d'aplat marine) */
2722 +.chip-key {
2723 + background: transparent; color: var(--ink); border: 1px solid var(--hairline-strong);
2724 + gap: 7px; font-weight: 600; padding: 7px 13px;
2725 +}
2726 +.chip-key svg { color: var(--accent-deep); flex: none; }
2727 +/* ancres en onglets textes */
2728 +.ancres { gap: 18px; border-bottom: 1px solid var(--hairline); }
2729 +.ancres a { padding: 10px 2px; border-radius: 0; font-family: var(--font-display); font-weight: 700; font-size: 13px; color: var(--ink-3); }
2730 +.ancres a:hover { background: none; color: var(--accent-deep); box-shadow: inset 0 -2.5px 0 var(--accent); }
2731 +/* inclusions : grille d'items en filets — chaque commodité a SON icône */
2732 +.amenity-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); gap: 0 28px; margin-bottom: 8px; }
2733 +.amenity-it {
2734 + display: flex; align-items: center; gap: 11px; padding: 7px 0;
2735 + border-bottom: 1px solid var(--hairline); font-size: 13.5px; color: var(--ink); min-height: 46px;
2736 +}
2737 +.am-ico {
2738 + display: inline-grid; place-items: center; width: 32px; height: 32px;
2739 + border-radius: 10px; background: var(--accent-soft); color: var(--accent-deep); flex: none;
2740 +}
2741 +.am-txt { min-width: 0; }
2742 +.am-conf { margin-left: auto; color: var(--green); font-weight: 700; font-size: 12px; }
2743 +.amenity-it.unconfirmed { color: var(--ink-2); }
2744 +.amenity-it.unconfirmed .am-ico { background: var(--surface-2); color: var(--ink-3); }
2745 +/* détails pratiques : rangées en filets (plus de boîtes grises) */
2746 +.kv { gap: 0 30px; margin: 14px 0 0; }
2747 +.kv .cell {
2748 + background: transparent; border-radius: 0; padding: 9px 0;
2749 + border-bottom: 1px solid var(--hairline);
2750 + display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
2751 +}
2752 +.kv .cell .k { font-family: var(--font-mono); }
2753 +.kv .cell .v { margin-top: 0; text-align: right; }
2754 +/* galerie : cadre allégé, rayon photo 10 */
2755 +.carousel { border: 1px solid var(--hairline-strong); box-shadow: none; border-radius: var(--r-2); }
2756 +.thumbs button { border-radius: 8px; }
2757 +/* « en bref » : filet accent au lieu d'une boîte pleine */
2758 +.enbref { background: transparent; border: 0; border-left: 3px solid var(--accent); border-radius: 0; padding: 2px 0 2px 14px; }
2759 +/* CTA sticky : sur la fiche, il prend le bas d'écran — la tabbar s'efface */
2760 +body:has(.cta-sticky) .tabbar { display: none; }
2761 +.cta-sticky { border-top: 1px solid var(--hairline-strong); background: rgba(250, 249, 245, 0.92); }
2762 +@media (max-width: 760px) {
2763 + body:has(.cta-sticky) { padding-bottom: 0; }
2764 +}
2765 +
2766 +/* colonne éditoriale (gauche) : les sections respirent sans boîte —
2767 + les modules de données de la colonne droite restent en cartes */
2768 +.fiche > .f-col:first-child > .f-bloc:not(.f-galerie):not(.f-hero) {
2769 + background: transparent; border: 0; border-radius: 0; padding: 0; box-shadow: none;
2770 +}
2771 +@media (max-width: 640px) {
2772 + .fiche > .f-col:first-child > .f-bloc:not(.f-galerie):not(.f-hero) { padding: 0; }
2773 +}
2774 +/* cellules kv : neutraliser la tuile (bord encre + ombre) de la couche fiche */
2775 +.kv .cell { border: 0; border-bottom: 1px solid var(--hairline); box-shadow: none; }
2776 +/* tiret accent des titres : sans bordure ni ombre héritées de la couche fiche */
2777 +.f-bloc > h2::before, .f-bloc h2::before, .quartier h2::before { border: 0; box-shadow: none; }
2714 2778