← back to Stayclaim
src/components/MiniMap.tsx
100 lines
/**
* MiniMap — server-rendered SVG map.
* Auto-fits a bounding box around the pins with a touch of padding.
* Same ink/sand/moss/coral palette as MapHero so the visual lineage is consistent.
*
* For multi-city pins (e.g. architect across LA), uses a wider bbox.
* For a single pin, falls back to a fixed-zoom Beverly Hills frame.
*/
export type MiniPin = {
lat: number;
lng: number;
label?: string;
featured?: boolean;
};
export function MiniMap({
pins,
height = 180,
className = '',
caption,
}: {
pins: MiniPin[];
height?: number;
className?: string;
caption?: string;
}) {
if (pins.length === 0) return null;
// bbox with min span so a single pin doesn't render a degenerate map
const lats = pins.map(p => p.lat);
const lngs = pins.map(p => p.lng);
let minLat = Math.min(...lats);
let maxLat = Math.max(...lats);
let minLng = Math.min(...lngs);
let maxLng = Math.max(...lngs);
const minSpan = 0.04;
if (maxLat - minLat < minSpan) {
const c = (maxLat + minLat) / 2;
minLat = c - minSpan / 2;
maxLat = c + minSpan / 2;
}
if (maxLng - minLng < minSpan) {
const c = (maxLng + minLng) / 2;
minLng = c - minSpan / 2;
maxLng = c + minSpan / 2;
}
// 8% padding on each axis
const padLat = (maxLat - minLat) * 0.08;
const padLng = (maxLng - minLng) * 0.08;
minLat -= padLat;
maxLat += padLat;
minLng -= padLng;
maxLng += padLng;
const w = 420;
const h = height;
const project = (p: MiniPin) => ({
x: ((p.lng - minLng) / (maxLng - minLng)) * w,
y: ((maxLat - p.lat) / (maxLat - minLat)) * h,
});
return (
<figure className={`block ${className}`}>
<svg
viewBox={`0 0 ${w} ${h}`}
preserveAspectRatio="xMidYMid slice"
width="100%"
height={h}
role="img"
aria-label={`Map with ${pins.length} ${pins.length === 1 ? 'location' : 'locations'}`}
className="bg-ink"
>
<defs>
<pattern id="mini-grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#f6f1e818" strokeWidth="0.5" />
</pattern>
</defs>
<rect width={w} height={h} fill="url(#mini-grid)" />
{pins.map((p, i) => {
const { x, y } = project(p);
const r = p.featured ? 5 : 4;
const fill = p.featured ? '#d4664a' : '#4a5d45';
return (
<g key={i}>
<circle cx={x} cy={y} r={r + 2} fill={fill} fillOpacity={0.25} />
<circle cx={x} cy={y} r={r} fill={fill} stroke="#f6f1e8" strokeWidth={1} />
</g>
);
})}
</svg>
{caption && (
<figcaption className="mt-2 font-mono text-[10px] uppercase tracking-[0.15em] text-ink/50">
{caption}
</figcaption>
)}
</figure>
);
}