simpse / src /components /LeafletMap.tsx
canboi99's picture
Deploy Simpse real estate platform with Vancouver multifamily building interactive map
0d2425e verified
Raw
History Blame Contribute Delete
7.71 kB
"use client"
import { useEffect, useRef, useState } from "react"
import L from "leaflet"
import { Badge } from "@/components/ui/badge"
// Fix for default markers in Leaflet
import "leaflet/dist/leaflet.css"
// Available tile providers (no API keys required)
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'
}
}
// Sample property data for Canadian cities
const sampleProperties = [
{
id: 1,
coordinates: [49.2827, -123.1207] as [number, number], // Vancouver
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], // Toronto
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], // Calgary
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], // Saskatoon
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' // green
case 'pending':
return '#F59E0B' // orange
case 'sold':
return '#EF4444' // red
default:
return '#3B82F6' // blue
}
}
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
// Initialize the map
mapRef.current = L.map(mapContainerRef.current).setView([56.1304, -96.0000], 4)
// Add selected tile provider
const provider = tileProviders[currentTileProvider]
tileLayerRef.current = L.tileLayer(provider.url, {
attribution: provider.attribution,
subdomains: 'abcd',
maxZoom: 19
}).addTo(mapRef.current)
// Add property markers
sampleProperties.forEach(property => {
const marker = L.marker(property.coordinates, {
icon: createCustomIcon(property)
}).addTo(mapRef.current!)
marker.on('click', () => {
onPropertySelect(property)
// Fly to the clicked property
mapRef.current?.setView(property.coordinates, 12, { animate: true })
})
markersRef.current.push(marker)
})
// Cleanup function
return () => {
if (mapRef.current) {
mapRef.current.remove()
mapRef.current = null
}
markersRef.current = []
}
}, [onPropertySelect, currentTileProvider])
// Handle tile provider changes
useEffect(() => {
if (!mapRef.current || !tileLayerRef.current) return
// Remove current tile layer
mapRef.current.removeLayer(tileLayerRef.current)
// Add new tile layer
const provider = tileProviders[currentTileProvider]
tileLayerRef.current = L.tileLayer(provider.url, {
attribution: provider.attribution,
subdomains: 'abcd',
maxZoom: 19
}).addTo(mapRef.current)
}, [currentTileProvider])
// Update marker styles when selectedProperty changes
useEffect(() => {
markersRef.current.forEach((marker, index) => {
const property = sampleProperties[index]
const isSelected = selectedProperty?.id === property.id
const icon = createCustomIcon(property)
if (isSelected) {
// Make selected marker larger and add pulse effect
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>
</>
)
}