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
3 days agolast push
HTML 98.9% Python 0.6%

Carte 3D réaliste (Standard pleine couleur + repères 3D) ; fiche : section Emplacement — mini-carte 3D zoomée sur l'appart (ListingMap3D, lazy), immeuble de l'annonce surligné en rouge via KaSpotlightMap, légende + ancre Carte

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

5 changed files +115 −2

added frontend/src/components/ListingMap3D.tsx +58 −0
@@ -0,0 +1,58 @@
1 +// -----------------------------------------------------------------------------
2 +// Author: Simon-Pierre Boucher
3 +// Contact: contact@spboucher.ai
4 +// Project: Groupe Ka / Ka Maps (Lou-Ka integration)
5 +// components/ListingMap3D.tsx : mini-carte 3D de la fiche — caméra serrée
6 +// sur l'adresse (zoom 17, inclinaison 62°), fond Mapbox Standard réaliste,
7 +// et l'immeuble de l'annonce surligné EN ROUGE (featureset « buildings »
8 +// via KaSpotlightMap). Chargée paresseusement depuis Listing.tsx.
9 +// -----------------------------------------------------------------------------
10 +import { useMemo } from "react";
11 +import "mapbox-gl/dist/mapbox-gl.css";
12 +import "@groupe-ka/ka-maps/styles.css";
13 +import type { MapProperty } from "@groupe-ka/ka-maps";
14 +import { KaBrandBadge, KaSpotlightMap } from "@groupe-ka/ka-maps/react";
15 +import type { Listing } from "../api";
16 +import { louKaMapTheme } from "../kamaps/theme";
17 +import { MAPBOX_TOKEN } from "../kamaps/config";
18 +
19 +/** Rouge signal de la fiche — même langage que les badges d'alerte. */
20 +export const BUILDING_RED = "#e03131";
21 +
22 +export default function ListingMap3D({ l }: { l: Listing }) {
23 + const property = useMemo<MapProperty | null>(() => {
24 + if (l.lat == null || l.lng == null) return null;
25 + return {
26 + id: l.uid,
27 + appSource: "lou-ka",
28 + latitude: l.lat,
29 + longitude: l.lng,
30 + kind: "listing",
31 + listingType: "rent",
32 + price: l.price ?? undefined,
33 + propertyType: l.unit_type || undefined,
34 + address: l.address || l.title || undefined,
35 + city: l.city || undefined,
36 + thumbnailUrl: l.images?.[0],
37 + };
38 + }, [l.uid, l.lat, l.lng, l.price, l.unit_type, l.address, l.title, l.city, l.images]);
39 +
40 + if (!property) return null;
41 +
42 + return (
43 + <div className="lmap3d" role="img"
44 + aria-label={`Carte 3D — ${l.address || l.title}, bâtiment de l'annonce en rouge`}>
45 + <KaSpotlightMap
46 + theme={louKaMapTheme}
47 + mapboxToken={MAPBOX_TOKEN}
48 + property={property}
49 + buildingColor={BUILDING_RED}
50 + >
51 + <KaBrandBadge />
52 + </KaSpotlightMap>
53 + <span className="lmap3d-legende" aria-hidden="true">
54 + <i /> Immeuble de l'annonce
55 + </span>
56 + </div>
57 + );
58 +}
modified frontend/src/components/MapView.tsx +3 −0
@@ -131,6 +131,9 @@ export default function MapView({
131 131 center={initialCamera}
132 132 zoom={initialCamera.zoom}
133 133 pitch={50}
134 + // Rendu réaliste : Standard pleine couleur + repères 3D (stades,
135 + // ponts, églises) — les pastilles encre/blanc gardent leur contraste.
136 + basemap={{ theme: "default", showLandmarks: true }}
134 137 cluster={{ maxZoom: 15, valueClamp: [250, 8000] }}
135 138 searchMode="manual"
136 139 onMoveEnd={onMoveEnd}
modified frontend/src/pages/Listing.tsx +18 −1
@@ -8,12 +8,15 @@
8 8 // Desktop : deux colonnes (logement à gauche, quartier/synthèse à droite)
9 9 // via wrappers `display:contents` + `order` (voir styles.css « fiche v2 »).
10 10 // -----------------------------------------------------------------------------
11 −import { useEffect, useRef, useState } from "react";
11 +import { lazy, Suspense, useEffect, useRef, useState } from "react";
12 12 import { Link, useParams } from "react-router-dom";
13 13 import { Listing, fetchListing, fetchSources, fmtAvailability, fmtDist, fmtPrice, registerSourceNames, sourceName } from "../api";
14 14 import QuartierBlock from "../components/QuartierBlock";
15 15 import { IcoAlert, IcoBuilding, IcoDoc } from "../components/Icons";
16 16
17 +// Mini-carte 3D (Mapbox) — chargée paresseusement, comme la grande carte.
18 +const ListingMap3D = lazy(() => import("../components/ListingMap3D"));
19 +
17 20 // Icônes et libellés des commodités de proximité (louka/poi.py)
18 21 const POI_META: Record<string, { icon: string; label: string }> = {
19 22 epicerie: { icon: "🛒", label: "Épicerie" },
@@ -302,6 +305,7 @@ export default function ListingPage() {
302 305 <nav className="ancres" aria-label="Sections de la fiche">
303 306 <a href="#description">Description</a>
304 307 <a href="#inclusions">Inclusions</a>
308 + {l.lat != null && l.lng != null && <a href="#emplacement">Carte</a>}
305 309 <a href="#quartier">Quartier</a>
306 310 <a href="#proximite">À proximité</a>
307 311 </nav>
@@ -331,6 +335,19 @@ export default function ListingPage() {
331 335 )}
332 336 </section>
333 337
338 + {l.lat != null && l.lng != null && (
339 + <section className="f-bloc f-carte" id="emplacement">
340 + <h2>Emplacement</h2>
341 + <Suspense fallback={<div className="lmap3d lmap3d-skel" aria-busy="true" />}>
342 + <ListingMap3D l={l} />
343 + </Suspense>
344 + <p className="fine">
345 + Vue 3D du secteur — l'immeuble de l'annonce est surligné en rouge.
346 + Position selon l'adresse géocodée (Adresses Québec).
347 + </p>
348 + </section>
349 + )}
350 +
334 351 <section className="f-bloc f-quartier" id="quartier">
335 352 {l.quartier ? <QuartierBlock q={l.quartier} /> : null}
336 353 </section>
modified frontend/src/styles.css +35 −0
@@ -1372,6 +1372,41 @@ html { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */
1372 1372 }
1373 1373 .mapview { position: relative; overflow: hidden; }
1374 1374 .mapview .ka-map { position: absolute; inset: 0; }
1375 +
1376 +/* mini-carte 3D de la fiche (Emplacement) — bâtiment de l'annonce en rouge */
1377 +.lmap3d {
1378 + position: relative; height: 340px; border: 1.5px solid var(--line-strong);
1379 + border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);
1380 + overflow: hidden; background: var(--surface-2);
1381 +}
1382 +.lmap3d .ka-map {
1383 + --ka-accent: var(--ink);
1384 + --ka-on-accent: var(--lime);
1385 + --ka-surface: var(--surface);
1386 + --ka-ink: var(--ink);
1387 + --ka-line: var(--line-strong);
1388 + --ka-radius: var(--r-ctl);
1389 + --ka-shadow: 4px 4px 0 rgba(20, 24, 20, 0.18);
1390 + --ka-font: var(--font-body);
1391 + position: absolute; inset: 0;
1392 +}
1393 +.lmap3d-skel {
1394 + background: linear-gradient(90deg, #eeece5 25%, #f7f5ef 50%, #eeece5 75%);
1395 + background-size: 800px 100%; animation: shimmer 1.4s infinite linear;
1396 +}
1397 +.lmap3d-legende {
1398 + position: absolute; left: 10px; bottom: 10px; z-index: 5;
1399 + display: inline-flex; align-items: center; gap: 6px;
1400 + padding: 4px 9px; border: 1.5px solid var(--line-strong);
1401 + border-radius: var(--r-ctl); background: var(--surface);
1402 + font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-2);
1403 + pointer-events: none;
1404 +}
1405 +.lmap3d-legende i {
1406 + width: 10px; height: 10px; border-radius: 3px;
1407 + background: #e03131; border: 1px solid rgba(20, 24, 20, 0.35);
1408 +}
1409 +@media (max-width: 780px) { .lmap3d { height: 280px; } }
1375 1410 .ka-preview .mv-pop img, .ka-preview .mv-noimg { height: 120px; }
1376 1411 .ka-preview-sheet .mv-pop { display: grid; grid-template-columns: 130px 1fr; }
1377 1412 .ka-preview-sheet .mv-pop img, .ka-preview-sheet .mv-noimg { height: 100%; min-height: 120px; }
modified frontend/tsconfig.tsbuildinfo +1 −1
@@ -1 +1 @@
1 −{"root":["./src/app.tsx","./src/account.tsx","./src/api.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/cookieconsent.tsx","./src/components/icons.tsx","./src/components/listingcard.tsx","./src/components/mapview.tsx","./src/components/pager.tsx","./src/components/quartierblock.tsx","./src/kamaps/adapter.ts","./src/kamaps/config.ts","./src/kamaps/theme.ts","./src/pages/bienvenue.tsx","./src/pages/bot.tsx","./src/pages/favoris.tsx","./src/pages/gestion.tsx","./src/pages/gestionpublic.tsx","./src/pages/home.tsx","./src/pages/listing.tsx","./src/pages/passerelle.tsx","./src/pages/privacy.tsx","./src/pages/profile.tsx","./src/pages/publicprofile.tsx","./src/pages/sources.tsx","./src/pages/stats.tsx","./src/pages/terms.tsx","./src/pages/ville.tsx"],"version":"5.9.3"}
\ No newline at end of file
1 +{"root":["./src/app.tsx","./src/account.tsx","./src/api.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/cookieconsent.tsx","./src/components/icons.tsx","./src/components/listingcard.tsx","./src/components/listingmap3d.tsx","./src/components/mapview.tsx","./src/components/pager.tsx","./src/components/quartierblock.tsx","./src/kamaps/adapter.ts","./src/kamaps/config.ts","./src/kamaps/theme.ts","./src/pages/bienvenue.tsx","./src/pages/bot.tsx","./src/pages/favoris.tsx","./src/pages/gestion.tsx","./src/pages/gestionpublic.tsx","./src/pages/home.tsx","./src/pages/listing.tsx","./src/pages/passerelle.tsx","./src/pages/privacy.tsx","./src/pages/profile.tsx","./src/pages/publicprofile.tsx","./src/pages/sources.tsx","./src/pages/stats.tsx","./src/pages/terms.tsx","./src/pages/ville.tsx"],"version":"5.9.3"}
\ No newline at end of file
2 2