SPB Git forge

spb/rent-ka

Public
8commits 1branches 0releases
7.4 MBsize
maindefault branch
19 days agolast push
Python 68.8% TypeScript 18.6% CSS 8.7% JavaScript 3.3% HTML 0.6%
5.7 KB · 156 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Author: Simon-Pierre Boucher3// Contact: contact@spboucher.ai4// Project: Groupe Ka / Ka Maps (Rent-Ka integration)5// components/MapView.tsx: Rent-Ka Maps — the product map, powered by6// le framework partagé Ka Maps (moteur MapLibre, pastilles de prix GPU,7// clustering natif, « Rechercher dans cette zone », aperçu React).8//   · chargée paresseusement (React.lazy) comme avant ;9//   · données pilotées par le viewport : /api/listings.geojson?bbox=…10//     (requêtes annulables, cache, jamais de tempête pendant le pan) ;11//   · caméra partageable dans l'URL (?lat=…&lng=…&zoom=…).12// -----------------------------------------------------------------------------13import { useCallback, useEffect, useMemo } from "react";14import { useNavigate } from "react-router-dom";15import "mapbox-gl/dist/mapbox-gl.css";16import "@groupe-ka/ka-maps/styles.css";17import type { MapProperty } from "@groupe-ka/ka-maps";18import { cameraFromParams, cameraToParams } from "@groupe-ka/ka-maps";19import {20  KaBrandBadge,21  KaMapView,22  LoadingIndicator,23  PropertyPreview,24  ResultCount,25  SearchAreaControl,26  Tilt3DControl,27  useKaMap,28} from "@groupe-ka/ka-maps/react";29import { ListingFilters, sourceName } from "../api";30import { louKaMapTheme } from "../kamaps/theme";31import { louKaMapAdapter } from "../kamaps/adapter";32import { MAPBOX_TOKEN } from "../kamaps/config";3334const CANADA_CENTER = { lat: 47.5, lng: -84.5 };3536const fmtDispo = (iso: string | null | undefined): string =>37  !iso ? "" : iso === "now" ? "Available now"38    : `Available ${new Date(iso + "T12:00:00").toLocaleDateString("en-CA", { day: "numeric", month: "short", year: "numeric" })}`;3940/** Compact rental preview — same visual language as ListingCard. */41function PreviewCard({ p }: { p: MapProperty }) {42  const navigate = useNavigate();43  const extra = (p.extra ?? {}) as {44    title?: string | null; priceLabel?: string | null;45    availabilityDate?: string | null; source?: string; areaSqft?: number | null;46  };47  const prix = p.price != null48    ? `$${p.price.toLocaleString("en-CA", { maximumFractionDigits: 0 })}`49    : extra.priceLabel || "Price on request";50  const meta = [51    p.propertyType,52    extra.areaSqft ? `${Math.round(extra.areaSqft)} sq ft` : "",53    fmtDispo(extra.availabilityDate),54  ].filter(Boolean).join(" · ");55  return (56    <div className="mv-pop">57      {p.thumbnailUrl ? (58        <img src={p.thumbnailUrl} alt="" loading="lazy" />59      ) : (60        <div className="mv-noimg" aria-hidden="true">⌂</div>61      )}62      <div className="mv-pop-body">63        <div className="mv-pop-price">64          {prix}65          <small>{p.price != null ? " /month" : ""}</small>66        </div>67        <div className="mv-pop-title">{extra.title ?? p.address ?? ""}</div>68        <div className="mv-pop-meta">{meta}</div>69        <div className="mv-pop-src">{sourceName(extra.source ?? "")}</div>70        <a71          className="mv-pop-cta"72          href={p.originalUrl}73          onClick={(e) => {74            e.preventDefault();75            navigate(p.originalUrl ?? "/");76          }}77        >78          See the listing →79        </a>80      </div>81    </div>82  );83}8485/** Pont déclaratif : reflète la sélection/le survol venus de la liste. */86function SelectionBridge({ selectedUid, hoveredUid }: {87  selectedUid: string | null;88  hoveredUid: string | null;89}) {90  const map = useKaMap();91  useEffect(() => { map?.select(selectedUid, "app"); }, [map, selectedUid]);92  useEffect(() => { map?.setHovered(hoveredUid, "app"); }, [map, hoveredUid]);93  return null;94}9596export interface MapViewProps {97  filters: ListingFilters;98  selectedUid?: string | null;99  hoveredUid?: string | null;100  onSelect?: (uid: string | null) => void;101}102103export default function MapView({104  filters, selectedUid = null, hoveredUid = null, onSelect,105}: MapViewProps) {106  // initial camera: shared URL (?lat&lng&zoom) else Canada107  const initialCamera = useMemo(() => {108    const cam = cameraFromParams(new URLSearchParams(window.location.search));109    return cam ?? { ...CANADA_CENTER, zoom: 4.4 };110  }, []);111112  // caméra → URL (replaceState : pas de re-render du routeur)113  const onMoveEnd = useCallback((center: { lat: number; lng: number }, zoom: number) => {114    const url = new URL(window.location.href);115    url.search = cameraToParams({ ...center, zoom }, url.searchParams).toString();116    window.history.replaceState(null, "", url);117  }, []);118119  const mapFilters = useMemo(120    () => Object.fromEntries(Object.entries(filters).filter(([, v]) => v)),121    [filters],122  );123124  return (125    <div className="mapview" role="application" aria-label="Rentals map">126      <KaMapView127        theme={louKaMapTheme}128        adapter={louKaMapAdapter}129        mapboxToken={MAPBOX_TOKEN}130        filters={mapFilters}131        center={initialCamera}132        zoom={initialCamera.zoom}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 }}137        cluster={{ maxZoom: 15, valueClamp: [250, 8000] }}138        searchMode="manual"139        onMoveEnd={onMoveEnd}140        onSelect={(p) => onSelect?.(p?.id ?? null)}141      >142        <SelectionBridge selectedUid={selectedUid} hoveredUid={hoveredUid} />143        <KaBrandBadge />144        <Tilt3DControl />145        <SearchAreaControl />146        <LoadingIndicator label="Updating rentals…" />147        <ResultCount148          emptyTitle="No rentals found in this area."149          emptyHint="Widen the map or change your filters."150        />151        <PropertyPreview render={(p) => <PreviewCard p={p} />} />152      </KaMapView>153    </div>154  );155}156