| "use client" |
|
|
| import { useEffect, useRef, useState } from "react" |
| import L from "leaflet" |
| import { Badge } from "@/components/ui/badge" |
|
|
| |
| import "leaflet/dist/leaflet.css" |
|
|
| |
| const tileProviders = { |
| osm: { |
| name: "OpenStreetMap", |
| url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' |
| }, |
| carto: { |
| name: "CartoDB Light", |
| url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>' |
| }, |
| cartoDark: { |
| name: "CartoDB Dark", |
| url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>' |
| }, |
| stamen: { |
| name: "Stamen Toner", |
| url: "https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}{r}.png", |
| attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a> — Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' |
| } |
| } |
|
|
| |
| const sampleProperties = [ |
| { |
| id: 1, |
| coordinates: [49.2827, -123.1207] as [number, number], |
| price: 1250000, |
| type: "Detached", |
| bedrooms: 4, |
| bathrooms: 3, |
| sqft: 2500, |
| address: "1234 Maple Street, Vancouver, BC", |
| status: "active" |
| }, |
| { |
| id: 2, |
| coordinates: [43.6532, -79.3832] as [number, number], |
| price: 890000, |
| type: "Condo", |
| bedrooms: 2, |
| bathrooms: 2, |
| sqft: 1200, |
| address: "567 Bay Street, Toronto, ON", |
| status: "active" |
| }, |
| { |
| id: 3, |
| coordinates: [51.0447, -114.0719] as [number, number], |
| price: 645000, |
| type: "Townhouse", |
| bedrooms: 3, |
| bathrooms: 2.5, |
| sqft: 1800, |
| address: "890 Pine Road, Calgary, AB", |
| status: "pending" |
| }, |
| { |
| id: 4, |
| coordinates: [52.1332, -106.3468] as [number, number], |
| price: 425000, |
| type: "Detached", |
| bedrooms: 3, |
| bathrooms: 2, |
| sqft: 1600, |
| address: "456 Oak Avenue, Saskatoon, SK", |
| status: "sold" |
| } |
| ] |
|
|
| interface LeafletMapProps { |
| onPropertySelect: (property: any) => void |
| selectedProperty: any |
| } |
|
|
| export default function LeafletMap({ onPropertySelect, selectedProperty }: LeafletMapProps) { |
| const mapRef = useRef<L.Map | null>(null) |
| const mapContainerRef = useRef<HTMLDivElement>(null) |
| const markersRef = useRef<L.Marker[]>([]) |
| const tileLayerRef = useRef<L.TileLayer | null>(null) |
| const [currentTileProvider, setCurrentTileProvider] = useState<keyof typeof tileProviders>('carto') |
|
|
| const formatCurrency = (value: number) => { |
| return new Intl.NumberFormat('en-CA', { |
| style: 'currency', |
| currency: 'CAD', |
| minimumFractionDigits: 0, |
| maximumFractionDigits: 0, |
| }).format(value) |
| } |
|
|
| const getMarkerColor = (status: string) => { |
| switch (status) { |
| case 'active': |
| return '#10B981' |
| case 'pending': |
| return '#F59E0B' |
| case 'sold': |
| return '#EF4444' |
| default: |
| return '#3B82F6' |
| } |
| } |
|
|
| const createCustomIcon = (property: any) => { |
| const color = getMarkerColor(property.status) |
| const priceText = formatCurrency(property.price).replace('CAD', '').trim() |
| |
| return L.divIcon({ |
| html: ` |
| <div style=" |
| background: ${color}; |
| color: white; |
| padding: 4px 8px; |
| border-radius: 12px; |
| font-size: 12px; |
| font-weight: 600; |
| text-align: center; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.2); |
| white-space: nowrap; |
| border: 2px solid white; |
| "> |
| ${priceText} |
| </div> |
| `, |
| className: 'custom-property-marker', |
| iconSize: [120, 30], |
| iconAnchor: [60, 30] |
| }) |
| } |
|
|
| useEffect(() => { |
| if (!mapContainerRef.current || mapRef.current) return |
|
|
| |
| mapRef.current = L.map(mapContainerRef.current).setView([56.1304, -96.0000], 4) |
|
|
| |
| const provider = tileProviders[currentTileProvider] |
| tileLayerRef.current = L.tileLayer(provider.url, { |
| attribution: provider.attribution, |
| subdomains: 'abcd', |
| maxZoom: 19 |
| }).addTo(mapRef.current) |
|
|
| |
| sampleProperties.forEach(property => { |
| const marker = L.marker(property.coordinates, { |
| icon: createCustomIcon(property) |
| }).addTo(mapRef.current!) |
|
|
| marker.on('click', () => { |
| onPropertySelect(property) |
| |
| mapRef.current?.setView(property.coordinates, 12, { animate: true }) |
| }) |
|
|
| markersRef.current.push(marker) |
| }) |
|
|
| |
| return () => { |
| if (mapRef.current) { |
| mapRef.current.remove() |
| mapRef.current = null |
| } |
| markersRef.current = [] |
| } |
| }, [onPropertySelect, currentTileProvider]) |
|
|
| |
| useEffect(() => { |
| if (!mapRef.current || !tileLayerRef.current) return |
|
|
| |
| mapRef.current.removeLayer(tileLayerRef.current) |
| |
| |
| const provider = tileProviders[currentTileProvider] |
| tileLayerRef.current = L.tileLayer(provider.url, { |
| attribution: provider.attribution, |
| subdomains: 'abcd', |
| maxZoom: 19 |
| }).addTo(mapRef.current) |
| }, [currentTileProvider]) |
|
|
| |
| useEffect(() => { |
| markersRef.current.forEach((marker, index) => { |
| const property = sampleProperties[index] |
| const isSelected = selectedProperty?.id === property.id |
| |
| const icon = createCustomIcon(property) |
| if (isSelected) { |
| |
| icon.options.html = icon.options.html?.replace( |
| 'box-shadow: 0 2px 4px rgba(0,0,0,0.2);', |
| 'box-shadow: 0 2px 4px rgba(0,0,0,0.2), 0 0 0 4px rgba(59, 130, 246, 0.3); transform: scale(1.1);' |
| ) |
| } |
| marker.setIcon(icon) |
| }) |
| }, [selectedProperty]) |
|
|
| const flyToProperty = (coordinates: [number, number]) => { |
| if (mapRef.current) { |
| mapRef.current.setView(coordinates, 12, { animate: true }) |
| } |
| } |
|
|
| const resetView = () => { |
| if (mapRef.current) { |
| mapRef.current.setView([56.1304, -96.0000], 4, { animate: true }) |
| } |
| } |
|
|
| return ( |
| <> |
| <div ref={mapContainerRef} className="w-full h-full" /> |
| |
| {/* Map Style Selector */} |
| <div className="absolute top-4 right-4 z-[1000]"> |
| <select |
| value={currentTileProvider} |
| onChange={(e) => setCurrentTileProvider(e.target.value as keyof typeof tileProviders)} |
| className="px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" |
| > |
| {Object.entries(tileProviders).map(([key, provider]) => ( |
| <option key={key} value={key}> |
| {provider.name} |
| </option> |
| ))} |
| </select> |
| </div> |
| |
| <style jsx global>{` |
| .custom-property-marker { |
| background: transparent !important; |
| border: none !important; |
| } |
| `}</style> |
| </> |
| ) |
| } |