@burdenoff/microfe-housingvista 2026.930.1 → 2026.930.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"PropertyMapView.js","names":[],"sources":["../../../../src/housingvista/components/maps/PropertyMapView.tsx"],"sourcesContent":["/**\n * PropertyMapView — the HousingVista property map (BOFF-4617 frontend half).\n *\n * Renders every property with real coordinates as a price-labeled pin using\n * react-leaflet (the same fleet-wide OSM + divIcon pattern as\n * microfe-movethewheels). Panning/zooming does NOT auto-refetch on every\n * frame — the user reviews the new viewport and explicitly presses\n * \"Search this area\", which reports the new bounds up to the caller so it\n * can be sent to the server-side `bounds` filter (`HousingVistaPropertyFilterInput`)\n * and mirrored into the URL for shareable/bookmarkable map searches.\n * @module housingvista\n */\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { MapContainer, Marker, Popup, TileLayer, useMapEvents } from 'react-leaflet';\nimport type { LatLngBounds } from 'leaflet';\nimport { Bed, Bath, Square, RotateCcw, ShieldCheck } from 'lucide-react';\nimport { boundsFromLeaflet, createPropertyPinIcon, isRealCoord, OSM_ATTRIBUTION, OSM_TILE_URL } from './leaflet-setup';\nimport type { LeafletBoundsResult } from './leaflet-setup';\nimport { cn, formatCompactCurrency, formatNumber } from '../../utils/helpers';\nimport type { Property } from '../../types';\n\n/** SSR/lazy safety: only mount Leaflet (which touches the DOM) after mount. */\nfunction useIsMounted(): boolean {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n return mounted;\n}\n\ninterface BoundsWatcherProps {\n onChange: (bounds: LatLngBounds) => void;\n}\n\n/** Tracks the live viewport and reports it up on every pan/zoom settle. */\nfunction BoundsWatcher({ onChange }: BoundsWatcherProps) {\n const map = useMapEvents({\n moveend: () => onChange(map.getBounds()),\n zoomend: () => onChange(map.getBounds()),\n });\n // `map` (from useMapEvents) and `onChange` (the parent's setState setter) are\n // both referentially stable across renders, so this effectively fires once\n // on mount — reporting the initial viewport — without needing to suppress\n // the exhaustive-deps rule.\n useEffect(() => {\n onChange(map.getBounds());\n }, [map, onChange]);\n return null;\n}\n\nexport interface PropertyMapViewProps {\n properties: Property[];\n loading?: boolean;\n /** Center used when no properties have coordinates yet. */\n fallbackCenter?: [number, number];\n height?: number | string;\n className?: string;\n selectedIds?: Set<string>;\n onSelectProperty?: (id: string) => void;\n /** Called only when the user presses \"Search this area\", with the map's current viewport. */\n onSearchThisArea?: (bounds: LeafletBoundsResult) => void;\n}\n\n/**\n * PropertyMapView — split layout: map on top (or left on wide screens) with a\n * scrollable list of the same matching properties alongside, so a click on\n * either a pin or a list row highlights the other.\n */\nexport const PropertyMapView = ({\n properties,\n loading = false,\n fallbackCenter = [20.5937, 78.9629], // India centroid — sensible default for an unfiltered, empty map.\n height = 520,\n className,\n selectedIds,\n onSelectProperty,\n onSearchThisArea,\n}: PropertyMapViewProps) => {\n const mounted = useIsMounted();\n const [pendingBounds, setPendingBounds] = useState<LatLngBounds | null>(null);\n const [hoveredId, setHoveredId] = useState<string | null>(null);\n const searchedBoundsRef = useRef<LeafletBoundsResult | null>(null);\n\n const plottable = useMemo(\n () => properties.filter((p) => isRealCoord(p.location.coordinates.latitude, p.location.coordinates.longitude)),\n [properties]\n );\n\n const initialCenter: [number, number] = plottable.length\n ? [plottable[0].location.coordinates.latitude, plottable[0].location.coordinates.longitude]\n : fallbackCenter;\n\n const dirtySearch = useMemo(() => {\n if (!pendingBounds) return false;\n const b = boundsFromLeaflet(pendingBounds);\n const last = searchedBoundsRef.current;\n if (!last) return true;\n const eps = 0.0005;\n return (\n Math.abs(b.neLat - last.neLat) > eps ||\n Math.abs(b.neLng - last.neLng) > eps ||\n Math.abs(b.swLat - last.swLat) > eps ||\n Math.abs(b.swLng - last.swLng) > eps\n );\n }, [pendingBounds]);\n\n const handleSearchThisArea = () => {\n if (!pendingBounds || !onSearchThisArea) return;\n const b = boundsFromLeaflet(pendingBounds);\n searchedBoundsRef.current = b;\n onSearchThisArea(b);\n };\n\n const wrapperClass = cn('relative overflow-hidden rounded-card border border-border-seam bg-bg-sunken', className);\n\n return (\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]\">\n <div className={wrapperClass} style={{ height, width: '100%' }}>\n {!mounted ? (\n <div className=\"flex h-full w-full items-center justify-center\" aria-busy=\"true\">\n <div className=\"h-full w-full motion-safe:animate-pulse bg-bg-sunken\" />\n </div>\n ) : (\n <>\n <MapContainer\n center={initialCenter}\n zoom={plottable.length ? 11 : 5}\n scrollWheelZoom\n style={{ height: '100%', width: '100%' }}\n className=\"z-0\"\n >\n <TileLayer url={OSM_TILE_URL} attribution={OSM_ATTRIBUTION} />\n <BoundsWatcher onChange={setPendingBounds} />\n {plottable.map((property) => {\n const isSelected = hoveredId === property.id || selectedIds?.has(property.id);\n return (\n <Marker\n key={property.id}\n position={[property.location.coordinates.latitude, property.location.coordinates.longitude]}\n icon={createPropertyPinIcon(formatCompactCurrency(property.price.amount, property.price.currency), {\n selected: isSelected,\n rera: property.reraVerificationStatus === 'verified',\n })}\n eventHandlers={{\n click: () => onSelectProperty?.(property.id),\n mouseover: () => setHoveredId(property.id),\n mouseout: () => setHoveredId((cur) => (cur === property.id ? null : cur)),\n }}\n >\n <Popup className=\"hv-popup\">\n <div className=\"p-3\">\n <div className=\"flex items-center gap-1.5\">\n <p className=\"text-sm font-semibold text-text-primary\">{property.title}</p>\n {property.reraVerificationStatus === 'verified' && (\n <ShieldCheck className=\"h-3.5 w-3.5 shrink-0 text-status-success-text\" />\n )}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {[property.location.neighborhood, property.location.city].filter(Boolean).join(', ')}\n </p>\n <p className=\"mt-1 text-sm font-bold text-text-primary\">\n {formatCompactCurrency(property.price.amount, property.price.currency)}\n {property.price.type === 'rent' ? '/mo' : ''}\n </p>\n <button\n type=\"button\"\n onClick={() => onSelectProperty?.(property.id)}\n className=\"mt-2 text-xs font-medium text-[var(--color-action-primary-bg)] hover:underline\"\n >\n View details\n </button>\n </div>\n </Popup>\n </Marker>\n );\n })}\n </MapContainer>\n\n {onSearchThisArea && (\n <div className=\"pointer-events-none absolute inset-x-0 top-3 z-10 flex justify-center\">\n <button\n type=\"button\"\n onClick={handleSearchThisArea}\n disabled={!dirtySearch || loading}\n className={cn(\n 'pointer-events-auto inline-flex items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface px-3 py-1.5 text-xs font-medium shadow-elevation-2 transition-colors',\n dirtySearch && !loading\n ? 'text-text-primary hover:bg-bg-sunken'\n : 'cursor-not-allowed text-text-secondary opacity-70'\n )}\n >\n <RotateCcw className=\"h-3 w-3\" />\n {loading ? 'Searching…' : 'Search this area'}\n </button>\n </div>\n )}\n </>\n )}\n </div>\n\n {/* Compact list synced with the plotted pins */}\n <div\n className=\"flex flex-col gap-2 overflow-y-auto rounded-card border border-border-seam bg-bg-surface p-2\"\n style={{ height }}\n >\n {plottable.length === 0 && !loading && (\n <p className=\"p-4 text-center text-sm text-text-secondary\">\n No properties with a mapped location match the current filters.\n </p>\n )}\n {plottable.map((property) => (\n <button\n key={property.id}\n type=\"button\"\n onMouseEnter={() => setHoveredId(property.id)}\n onMouseLeave={() => setHoveredId((cur) => (cur === property.id ? null : cur))}\n onClick={() => onSelectProperty?.(property.id)}\n className={cn(\n 'flex items-center gap-3 rounded-lg border p-2 text-left transition-colors',\n hoveredId === property.id\n ? 'border-[var(--color-action-primary-bg)] bg-bg-sunken'\n : 'border-transparent hover:bg-bg-sunken'\n )}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{property.title}</p>\n <p className=\"truncate text-xs text-text-secondary\">\n {[property.location.neighborhood, property.location.city].filter(Boolean).join(', ')}\n </p>\n <div className=\"mt-1 flex items-center gap-3 text-[11px] text-text-secondary\">\n {property.specifications.bedrooms != null && (\n <span className=\"inline-flex items-center gap-1\">\n <Bed className=\"h-3 w-3\" />\n {property.specifications.bedrooms}\n </span>\n )}\n {property.specifications.bathrooms != null && (\n <span className=\"inline-flex items-center gap-1\">\n <Bath className=\"h-3 w-3\" />\n {property.specifications.bathrooms}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1\">\n <Square className=\"h-3 w-3\" />\n {formatNumber(property.specifications.totalArea)} sq ft\n </span>\n </div>\n </div>\n <span className=\"shrink-0 text-sm font-bold text-text-primary\">\n {formatCompactCurrency(property.price.amount, property.price.currency)}\n </span>\n </button>\n ))}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"PropertyMapView.js","names":[],"sources":["../../../../src/housingvista/components/maps/PropertyMapView.tsx"],"sourcesContent":["/**\n * PropertyMapView — the HousingVista property map (BOFF-4617 frontend half).\n *\n * Renders every property with real coordinates as a price-labeled pin using\n * react-leaflet (the same fleet-wide OSM + divIcon pattern as\n * microfe-movethewheels). Panning/zooming does NOT auto-refetch on every\n * frame — the user reviews the new viewport and explicitly presses\n * \"Search this area\", which reports the new bounds up to the caller so it\n * can be sent to the server-side `bounds` filter (`HousingVistaPropertyFilterInput`)\n * and mirrored into the URL for shareable/bookmarkable map searches.\n * @module housingvista\n */\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { MapContainer, Marker, Popup, TileLayer, useMapEvents } from 'react-leaflet';\nimport type { LatLngBounds } from 'leaflet';\nimport { Bed, Bath, Square, RotateCcw, ShieldCheck, BadgeCheck } from 'lucide-react';\nimport { boundsFromLeaflet, createPropertyPinIcon, isRealCoord, OSM_ATTRIBUTION, OSM_TILE_URL } from './leaflet-setup';\nimport type { LeafletBoundsResult } from './leaflet-setup';\nimport { cn, formatCompactCurrency, formatNumber } from '../../utils/helpers';\nimport type { Property } from '../../types';\n\n/** SSR/lazy safety: only mount Leaflet (which touches the DOM) after mount. */\nfunction useIsMounted(): boolean {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n return mounted;\n}\n\ninterface BoundsWatcherProps {\n onChange: (bounds: LatLngBounds) => void;\n}\n\n/** Tracks the live viewport and reports it up on every pan/zoom settle. */\nfunction BoundsWatcher({ onChange }: BoundsWatcherProps) {\n const map = useMapEvents({\n moveend: () => onChange(map.getBounds()),\n zoomend: () => onChange(map.getBounds()),\n });\n // `map` (from useMapEvents) and `onChange` (the parent's setState setter) are\n // both referentially stable across renders, so this effectively fires once\n // on mount — reporting the initial viewport — without needing to suppress\n // the exhaustive-deps rule.\n useEffect(() => {\n onChange(map.getBounds());\n }, [map, onChange]);\n return null;\n}\n\nexport interface PropertyMapViewProps {\n properties: Property[];\n loading?: boolean;\n /** Center used when no properties have coordinates yet. */\n fallbackCenter?: [number, number];\n height?: number | string;\n className?: string;\n selectedIds?: Set<string>;\n onSelectProperty?: (id: string) => void;\n /** Called only when the user presses \"Search this area\", with the map's current viewport. */\n onSearchThisArea?: (bounds: LeafletBoundsResult) => void;\n}\n\n/**\n * PropertyMapView — split layout: map on top (or left on wide screens) with a\n * scrollable list of the same matching properties alongside, so a click on\n * either a pin or a list row highlights the other.\n */\nexport const PropertyMapView = ({\n properties,\n loading = false,\n fallbackCenter = [20.5937, 78.9629], // India centroid — sensible default for an unfiltered, empty map.\n height = 520,\n className,\n selectedIds,\n onSelectProperty,\n onSearchThisArea,\n}: PropertyMapViewProps) => {\n const mounted = useIsMounted();\n const [pendingBounds, setPendingBounds] = useState<LatLngBounds | null>(null);\n const [hoveredId, setHoveredId] = useState<string | null>(null);\n const searchedBoundsRef = useRef<LeafletBoundsResult | null>(null);\n\n const plottable = useMemo(\n () => properties.filter((p) => isRealCoord(p.location.coordinates.latitude, p.location.coordinates.longitude)),\n [properties]\n );\n\n const initialCenter: [number, number] = plottable.length\n ? [plottable[0].location.coordinates.latitude, plottable[0].location.coordinates.longitude]\n : fallbackCenter;\n\n const dirtySearch = useMemo(() => {\n if (!pendingBounds) return false;\n const b = boundsFromLeaflet(pendingBounds);\n const last = searchedBoundsRef.current;\n if (!last) return true;\n const eps = 0.0005;\n return (\n Math.abs(b.neLat - last.neLat) > eps ||\n Math.abs(b.neLng - last.neLng) > eps ||\n Math.abs(b.swLat - last.swLat) > eps ||\n Math.abs(b.swLng - last.swLng) > eps\n );\n }, [pendingBounds]);\n\n const handleSearchThisArea = () => {\n if (!pendingBounds || !onSearchThisArea) return;\n const b = boundsFromLeaflet(pendingBounds);\n searchedBoundsRef.current = b;\n onSearchThisArea(b);\n };\n\n const wrapperClass = cn('relative overflow-hidden rounded-card border border-border-seam bg-bg-sunken', className);\n\n return (\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]\">\n <div className={wrapperClass} style={{ height, width: '100%' }}>\n {!mounted ? (\n <div className=\"flex h-full w-full items-center justify-center\" aria-busy=\"true\">\n <div className=\"h-full w-full motion-safe:animate-pulse bg-bg-sunken\" />\n </div>\n ) : (\n <>\n <MapContainer\n center={initialCenter}\n zoom={plottable.length ? 11 : 5}\n scrollWheelZoom\n style={{ height: '100%', width: '100%' }}\n className=\"z-0\"\n >\n <TileLayer url={OSM_TILE_URL} attribution={OSM_ATTRIBUTION} />\n <BoundsWatcher onChange={setPendingBounds} />\n {plottable.map((property) => {\n const isSelected = hoveredId === property.id || selectedIds?.has(property.id);\n return (\n <Marker\n key={property.id}\n position={[property.location.coordinates.latitude, property.location.coordinates.longitude]}\n icon={createPropertyPinIcon(formatCompactCurrency(property.price.amount, property.price.currency), {\n selected: isSelected,\n rera: property.reraVerificationStatus === 'verified',\n })}\n eventHandlers={{\n click: () => onSelectProperty?.(property.id),\n mouseover: () => setHoveredId(property.id),\n mouseout: () => setHoveredId((cur) => (cur === property.id ? null : cur)),\n }}\n >\n <Popup className=\"hv-popup\">\n <div className=\"p-3\">\n <div className=\"flex items-center gap-1.5\">\n <p className=\"text-sm font-semibold text-text-primary\">{property.title}</p>\n {property.reraVerificationStatus === 'verified' && (\n <ShieldCheck className=\"h-3.5 w-3.5 shrink-0 text-status-success-text\" />\n )}\n {property.verificationStatus === 'verified' && (\n <BadgeCheck\n className=\"h-3.5 w-3.5 shrink-0 text-status-success-text\"\n aria-label=\"Verified listing\"\n />\n )}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {[property.location.neighborhood, property.location.city].filter(Boolean).join(', ')}\n </p>\n <p className=\"mt-1 text-sm font-bold text-text-primary\">\n {formatCompactCurrency(property.price.amount, property.price.currency)}\n {property.price.type === 'rent' ? '/mo' : ''}\n </p>\n <button\n type=\"button\"\n onClick={() => onSelectProperty?.(property.id)}\n className=\"mt-2 text-xs font-medium text-[var(--color-action-primary-bg)] hover:underline\"\n >\n View details\n </button>\n </div>\n </Popup>\n </Marker>\n );\n })}\n </MapContainer>\n\n {onSearchThisArea && (\n <div className=\"pointer-events-none absolute inset-x-0 top-3 z-10 flex justify-center\">\n <button\n type=\"button\"\n onClick={handleSearchThisArea}\n disabled={!dirtySearch || loading}\n className={cn(\n 'pointer-events-auto inline-flex items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface px-3 py-1.5 text-xs font-medium shadow-elevation-2 transition-colors',\n dirtySearch && !loading\n ? 'text-text-primary hover:bg-bg-sunken'\n : 'cursor-not-allowed text-text-secondary opacity-70'\n )}\n >\n <RotateCcw className=\"h-3 w-3\" />\n {loading ? 'Searching…' : 'Search this area'}\n </button>\n </div>\n )}\n </>\n )}\n </div>\n\n {/* Compact list synced with the plotted pins */}\n <div\n className=\"flex flex-col gap-2 overflow-y-auto rounded-card border border-border-seam bg-bg-surface p-2\"\n style={{ height }}\n >\n {plottable.length === 0 && !loading && (\n <p className=\"p-4 text-center text-sm text-text-secondary\">\n No properties with a mapped location match the current filters.\n </p>\n )}\n {plottable.map((property) => (\n <button\n key={property.id}\n type=\"button\"\n onMouseEnter={() => setHoveredId(property.id)}\n onMouseLeave={() => setHoveredId((cur) => (cur === property.id ? null : cur))}\n onClick={() => onSelectProperty?.(property.id)}\n className={cn(\n 'flex items-center gap-3 rounded-lg border p-2 text-left transition-colors',\n hoveredId === property.id\n ? 'border-[var(--color-action-primary-bg)] bg-bg-sunken'\n : 'border-transparent hover:bg-bg-sunken'\n )}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{property.title}</p>\n <p className=\"truncate text-xs text-text-secondary\">\n {[property.location.neighborhood, property.location.city].filter(Boolean).join(', ')}\n </p>\n <div className=\"mt-1 flex items-center gap-3 text-[11px] text-text-secondary\">\n {property.specifications.bedrooms != null && (\n <span className=\"inline-flex items-center gap-1\">\n <Bed className=\"h-3 w-3\" />\n {property.specifications.bedrooms}\n </span>\n )}\n {property.specifications.bathrooms != null && (\n <span className=\"inline-flex items-center gap-1\">\n <Bath className=\"h-3 w-3\" />\n {property.specifications.bathrooms}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1\">\n <Square className=\"h-3 w-3\" />\n {formatNumber(property.specifications.totalArea)} sq ft\n </span>\n </div>\n </div>\n <span className=\"shrink-0 text-sm font-bold text-text-primary\">\n {formatCompactCurrency(property.price.amount, property.price.currency)}\n </span>\n </button>\n ))}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PropertiesPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/PropertiesPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA2CH,eAAO,MAAM,cAAc,mCAu9B1B,CAAC"}
1
+ {"version":3,"file":"PropertiesPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/PropertiesPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA4CH,eAAO,MAAM,cAAc,mCAqhC1B,CAAC"}