126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef } from "react"
|
|
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"
|
|
import L from "leaflet"
|
|
import "leaflet/dist/leaflet.css"
|
|
import { Restaurant } from "@/app/types"
|
|
import Link from "next/link"
|
|
|
|
// Custom amber marker
|
|
function createAmberIcon(isSelected: boolean) {
|
|
const size = isSelected ? 40 : 32
|
|
const svg = `
|
|
<svg width="${size}" height="${size}" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="16" cy="14" r="10" fill="${isSelected ? "#f59e0b" : "#d97706"}" stroke="${isSelected ? "#fff" : "#f59e0b"}" stroke-width="2"/>
|
|
<path d="M16 28 L10 20 Q16 22 22 20 Z" fill="${isSelected ? "#f59e0b" : "#d97706"}"/>
|
|
<circle cx="16" cy="14" r="4" fill="${isSelected ? "#000" : "#0d0d0d"}"/>
|
|
</svg>
|
|
`
|
|
return L.divIcon({
|
|
html: svg,
|
|
className: "",
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size],
|
|
popupAnchor: [0, -size],
|
|
})
|
|
}
|
|
|
|
function avgRating(r: Restaurant) {
|
|
if (!r.visits.length) return 0
|
|
return r.visits.reduce((s, v) => s + v.rating, 0) / r.visits.length
|
|
}
|
|
|
|
function FlyToSelected({
|
|
restaurants,
|
|
selectedId,
|
|
}: {
|
|
restaurants: Restaurant[]
|
|
selectedId: string | null
|
|
}) {
|
|
const map = useMap()
|
|
useEffect(() => {
|
|
if (!selectedId) return
|
|
const r = restaurants.find((x) => x.id === selectedId)
|
|
if (r) {
|
|
map.flyTo([r.lat, r.lng], 13, { duration: 1 })
|
|
}
|
|
}, [selectedId, restaurants, map])
|
|
return null
|
|
}
|
|
|
|
interface Props {
|
|
restaurants: Restaurant[]
|
|
selectedId: string | null
|
|
onSelect: (id: string | null) => void
|
|
}
|
|
|
|
export default function MapView({ restaurants, selectedId, onSelect }: Props) {
|
|
const center: [number, number] =
|
|
restaurants.length > 0
|
|
? [restaurants[0].lat, restaurants[0].lng]
|
|
: [51.5, 10]
|
|
|
|
return (
|
|
<MapContainer
|
|
center={center}
|
|
zoom={5}
|
|
style={{ height: "100%", width: "100%", background: "#0d0d0d" }}
|
|
zoomControl={false}
|
|
>
|
|
<TileLayer
|
|
attribution='© <a href="https://carto.com/">CARTO</a>'
|
|
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
|
subdomains="abcd"
|
|
maxZoom={20}
|
|
/>
|
|
|
|
<FlyToSelected restaurants={restaurants} selectedId={selectedId} />
|
|
|
|
{restaurants.map((r) => (
|
|
<Marker
|
|
key={r.id}
|
|
position={[r.lat, r.lng]}
|
|
icon={createAmberIcon(selectedId === r.id)}
|
|
eventHandlers={{
|
|
click: () => onSelect(r.id === selectedId ? null : r.id),
|
|
}}
|
|
>
|
|
<Popup>
|
|
<div className="min-w-[180px]">
|
|
<p className="font-bold text-[#f5f5f5] text-base leading-tight">{r.name}</p>
|
|
<p className="text-white/50 text-xs mt-1">
|
|
{r.city}, {r.country}
|
|
</p>
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
|
{r.cuisine.map((c) => (
|
|
<span
|
|
key={c}
|
|
className="text-[9px] uppercase tracking-wider text-[#f59e0b] bg-[#f59e0b]/15 rounded px-1.5 py-0.5 font-semibold"
|
|
>
|
|
{c}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center gap-3 mt-2">
|
|
<span className="text-[#f59e0b] font-black text-lg">
|
|
{avgRating(r).toFixed(1)}
|
|
</span>
|
|
<span className="text-white/40 text-xs">
|
|
{r.visits.length} {r.visits.length === 1 ? "visit" : "visits"}
|
|
</span>
|
|
</div>
|
|
<Link
|
|
href={`/restaurant/${r.id}`}
|
|
className="mt-3 block text-center text-xs font-bold text-black bg-[#f59e0b] rounded-lg py-1.5 hover:bg-[#d97706] transition-colors"
|
|
>
|
|
View details
|
|
</Link>
|
|
</div>
|
|
</Popup>
|
|
</Marker>
|
|
))}
|
|
</MapContainer>
|
|
)
|
|
}
|