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>
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 | |