fifteen/apps/web/components/map-legend.tsx

133 lines
4.3 KiB
TypeScript

"use client";
import type { OverlayMode } from "./location-score-panel";
import type { BaseOverlay } from "./map-view";
interface MapLegendProps {
overlayMode: OverlayMode;
baseOverlay: BaseOverlay;
threshold: number;
hasPinData: boolean;
}
// Shared score ramp stops — matches makeColorExpr in map-view.tsx
const SCORE_STOPS: [number, string][] = [
[0, "#d73027"],
[0.25, "#fc8d59"],
[0.5, "#fee08b"],
[0.75, "#d9ef8b"],
[1, "#1a9850"],
];
function gradientCss(stops: [number, string][]): string {
return `linear-gradient(to right, ${stops.map(([p, c]) => `${c} ${p * 100}%`).join(", ")})`;
}
export function MapLegend({ overlayMode, baseOverlay, threshold, hasPinData }: MapLegendProps) {
if (overlayMode === "isochrone" && hasPinData) {
// Travel-time legend: green (near) → red (far)
const stops: [number, string][] = [
[0, "#1a9850"],
[0.5, "#fee08b"],
[1, "#d73027"],
];
return (
<div className="absolute bottom-8 left-4 z-20 bg-white/90 backdrop-blur-sm rounded-lg shadow border border-gray-100 px-3 py-2 text-xs text-gray-600">
<div className="font-medium mb-1.5">Travel time</div>
<div className="flex items-center gap-2">
<span className="shrink-0">0 min</span>
<div
className="h-2.5 w-28 rounded-full"
style={{ background: gradientCss(stops) }}
/>
<span className="shrink-0">{threshold} min</span>
</div>
</div>
);
}
if (overlayMode === "relative" && hasPinData) {
const stops: [number, string][] = [
[0, "#d73027"],
[0.25, "#fc8d59"],
[0.5, "#ffffbf"],
[0.75, "#91cf60"],
[1, "#1a9850"],
];
return (
<div className="absolute bottom-8 left-4 z-20 bg-white/90 backdrop-blur-sm rounded-lg shadow border border-gray-100 px-3 py-2 text-xs text-gray-600">
<div className="font-medium mb-1.5">vs. pin</div>
<div className="flex items-center gap-2">
<span className="shrink-0">Worse</span>
<div
className="h-2.5 w-28 rounded-full"
style={{ background: gradientCss(stops) }}
/>
<span className="shrink-0">Better</span>
</div>
</div>
);
}
// Hidden gem score
if (baseOverlay === "hidden-gem" && !hasPinData) {
const stops: [number, string][] = [
[0, "#d73027"],
[0.4, "#fee08b"],
[0.7, "#d9ef8b"],
[1, "#1a9850"],
];
return (
<div className="absolute bottom-8 left-4 z-20 bg-white/90 backdrop-blur-sm rounded-lg shadow border border-gray-100 px-3 py-2 text-xs text-gray-600">
<div className="font-medium mb-1.5">Hidden Gem Score</div>
<div className="flex items-center gap-2">
<span className="shrink-0">Poor value</span>
<div
className="h-2.5 w-28 rounded-full"
style={{ background: gradientCss(stops) }}
/>
<span className="shrink-0">High value</span>
</div>
</div>
);
}
// Estate value (land price)
if (baseOverlay === "estate-value" && !hasPinData) {
const stops: [number, string][] = [
[0, "#ffffb2"],
[0.07, "#fecc5c"],
[0.2, "#fd8d3c"],
[0.47, "#f03b20"],
[1, "#bd0026"],
];
return (
<div className="absolute bottom-8 left-4 z-20 bg-white/90 backdrop-blur-sm rounded-lg shadow border border-gray-100 px-3 py-2 text-xs text-gray-600">
<div className="font-medium mb-1.5">Land Value</div>
<div className="flex items-center gap-2">
<span className="shrink-0">0</span>
<div
className="h-2.5 w-28 rounded-full"
style={{ background: gradientCss(stops) }}
/>
<span className="shrink-0">1500+ /m²</span>
</div>
</div>
);
}
// Default: absolute accessibility score
return (
<div className="absolute bottom-8 left-4 z-20 bg-white/90 backdrop-blur-sm rounded-lg shadow border border-gray-100 px-3 py-2 text-xs text-gray-600">
<div className="font-medium mb-1.5">Accessibility</div>
<div className="flex items-center gap-2">
<span className="shrink-0">Low</span>
<div
className="h-2.5 w-28 rounded-full"
style={{ background: gradientCss(SCORE_STOPS) }}
/>
<span className="shrink-0">High</span>
</div>
</div>
);
}