Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1// -----------------------------------------------------------------------------2// Lou-Ka — Location court terme3// components/CtMapView.tsx : carte des hébergements court terme (Ka Maps),4// pilotée par le viewport via /api/ct/listings.geojson — chargée paresseusement.5// -----------------------------------------------------------------------------6import { useCallback, useMemo } from "react";7import { useNavigate } from "react-router-dom";8import "mapbox-gl/dist/mapbox-gl.css";9import "@groupe-ka/ka-maps/styles.css";10import type { MapProperty } from "@groupe-ka/ka-maps";11import { cameraFromParams, cameraToParams } from "@groupe-ka/ka-maps";12import {13 KaBrandBadge,14 KaMapView,15 LoadingIndicator,16 PropertyPreview,17 ResultCount,18 SearchAreaControl,19 Tilt3DControl,20} from "@groupe-ka/ka-maps/react";21import { CtFilters, ctSourceName } from "../ctapi";22import { louKaMapTheme } from "../kamaps/theme";23import { ctMapAdapter } from "../kamaps/ctAdapter";24import { MAPBOX_TOKEN } from "../kamaps/config";2526const QUEBEC_CENTER = { lat: 46.82, lng: -71.25 };2728function PreviewCard({ p }: { p: MapProperty }) {29 const navigate = useNavigate();30 const extra = (p.extra ?? {}) as {31 title?: string | null; priceLabel?: string | null;32 capacity?: number | null; bedrooms?: number | null;33 rating?: number | null; source?: string;34 };35 const prix = p.price != null36 ? `${p.price.toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $`37 : extra.priceLabel || "Prix sur demande";38 const meta = [39 p.propertyType,40 extra.capacity ? `${extra.capacity} pers.` : "",41 extra.bedrooms ? `${extra.bedrooms} ch.` : "",42 extra.rating != null ? `★ ${extra.rating.toLocaleString("fr-CA", { maximumFractionDigits: 1 })}` : "",43 ].filter(Boolean).join(" · ");44 return (45 <div className="mv-pop">46 {p.thumbnailUrl ? (47 <img src={p.thumbnailUrl} alt="" loading="lazy" />48 ) : (49 <div className="mv-noimg" aria-hidden="true">⌂</div>50 )}51 <div className="mv-pop-body">52 <div className="mv-pop-price">53 {prix}54 <small>{p.price != null ? " /nuit" : ""}</small>55 </div>56 <div className="mv-pop-title">{extra.title ?? p.address ?? ""}</div>57 <div className="mv-pop-meta">{meta}</div>58 <div className="mv-pop-src">{ctSourceName(extra.source ?? "")}</div>59 <a60 className="mv-pop-cta"61 href={p.originalUrl}62 onClick={(e) => {63 e.preventDefault();64 navigate(p.originalUrl ?? "/court-terme");65 }}66 >67 Voir la fiche →68 </a>69 </div>70 </div>71 );72}7374export default function CtMapView({ filters }: { filters: CtFilters }) {75 const initialCamera = useMemo(() => {76 const cam = cameraFromParams(new URLSearchParams(window.location.search));77 return cam ?? { ...QUEBEC_CENTER, zoom: 6.2 };78 }, []);7980 const onMoveEnd = useCallback((center: { lat: number; lng: number }, zoom: number) => {81 const url = new URL(window.location.href);82 url.search = cameraToParams({ ...center, zoom }, url.searchParams).toString();83 window.history.replaceState(null, "", url);84 }, []);8586 const mapFilters = useMemo(87 () => Object.fromEntries(Object.entries(filters).filter(([, v]) => v)),88 [filters],89 );9091 return (92 <div className="mapview" role="application" aria-label="Carte des hébergements court terme">93 <KaMapView94 theme={louKaMapTheme}95 adapter={ctMapAdapter}96 mapboxToken={MAPBOX_TOKEN}97 filters={mapFilters}98 center={initialCamera}99 zoom={initialCamera.zoom}100 pitch={45}101 basemap={{ theme: "default", showLandmarks: true }}102 cluster={{ maxZoom: 15, valueClamp: [50, 2000] }}103 searchMode="manual"104 onMoveEnd={onMoveEnd}105 >106 <KaBrandBadge />107 <Tilt3DControl />108 <SearchAreaControl />109 <LoadingIndicator label="Mise à jour des hébergements…" />110 <ResultCount111 emptyTitle="Aucun hébergement trouvé dans cette zone."112 emptyHint="Élargissez la carte ou modifiez vos filtres."113 />114 <PropertyPreview render={(p) => <PreviewCard p={p} />} />115 </KaMapView>116 </div>117 );118}119