185 lines
4.7 KiB
TypeScript
185 lines
4.7 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import type { FeatureCollection } from "geojson";
|
||
|
||
import { useIsochrones } from "@/hooks/useIsochrones";
|
||
|
||
type IsoMode = "foot-walking" | "cycling-regular" | "driving-car";
|
||
|
||
interface Props {
|
||
cadNum: string;
|
||
onResult: (fc: FeatureCollection) => void;
|
||
}
|
||
|
||
const MODE_LABELS: Record<IsoMode, string> = {
|
||
"foot-walking": "Пешком",
|
||
"cycling-regular": "Велосипед",
|
||
"driving-car": "Машина",
|
||
};
|
||
|
||
const ALL_TIMES = [10, 15, 30] as const;
|
||
|
||
export function IsochronesPanel({ cadNum, onResult }: Props) {
|
||
const [mode, setMode] = useState<IsoMode>("foot-walking");
|
||
const [times, setTimes] = useState<Set<number>>(new Set([10, 15]));
|
||
|
||
const { mutate, isPending, isError, error } = useIsochrones();
|
||
|
||
function toggleTime(t: number) {
|
||
setTimes((prev) => {
|
||
const next = new Set(prev);
|
||
if (next.has(t)) {
|
||
next.delete(t);
|
||
} else {
|
||
next.add(t);
|
||
}
|
||
return next;
|
||
});
|
||
}
|
||
|
||
function handleShow() {
|
||
const timesMin = ALL_TIMES.filter((t) => times.has(t));
|
||
if (timesMin.length === 0) return;
|
||
mutate(
|
||
{ cadNum, mode, timesMin },
|
||
{
|
||
onSuccess: (res) => {
|
||
onResult(res.geojson);
|
||
},
|
||
},
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
padding: "12px 24px 16px",
|
||
borderTop: "1px solid #e5e7eb",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: "#6b7280",
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.05em",
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
Изохроны доступности
|
||
</div>
|
||
|
||
{/* Mode radio */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
gap: 6,
|
||
marginBottom: 10,
|
||
flexWrap: "wrap",
|
||
}}
|
||
>
|
||
{(Object.keys(MODE_LABELS) as IsoMode[]).map((m) => (
|
||
<label
|
||
key={m}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
fontSize: 13,
|
||
cursor: "pointer",
|
||
padding: "4px 10px",
|
||
borderRadius: 6,
|
||
border: `1px solid ${mode === m ? "#3b82f6" : "#e5e7eb"}`,
|
||
background: mode === m ? "#dbeafe" : "#fff",
|
||
color: mode === m ? "#1d4ed8" : "#374151",
|
||
userSelect: "none",
|
||
}}
|
||
>
|
||
<input
|
||
type="radio"
|
||
name="iso-mode"
|
||
value={m}
|
||
checked={mode === m}
|
||
onChange={() => setMode(m)}
|
||
style={{ display: "none" }}
|
||
/>
|
||
{MODE_LABELS[m]}
|
||
</label>
|
||
))}
|
||
</div>
|
||
|
||
{/* Time checkboxes */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
gap: 6,
|
||
marginBottom: 12,
|
||
flexWrap: "wrap",
|
||
}}
|
||
>
|
||
{ALL_TIMES.map((t) => {
|
||
const checked = times.has(t);
|
||
return (
|
||
<label
|
||
key={t}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
fontSize: 13,
|
||
cursor: "pointer",
|
||
padding: "4px 10px",
|
||
borderRadius: 6,
|
||
border: `1px solid ${checked ? "#3b82f6" : "#e5e7eb"}`,
|
||
background: checked ? "#dbeafe" : "#fff",
|
||
color: checked ? "#1d4ed8" : "#374151",
|
||
userSelect: "none",
|
||
}}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={checked}
|
||
onChange={() => toggleTime(t)}
|
||
style={{ display: "none" }}
|
||
/>
|
||
{t} мин
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Show button */}
|
||
<button
|
||
onClick={handleShow}
|
||
disabled={isPending || times.size === 0}
|
||
style={{
|
||
padding: "7px 16px",
|
||
borderRadius: 8,
|
||
border: "none",
|
||
background: isPending || times.size === 0 ? "#e5e7eb" : "#3b82f6",
|
||
color: isPending || times.size === 0 ? "#9ca3af" : "#fff",
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
cursor: isPending || times.size === 0 ? "not-allowed" : "pointer",
|
||
}}
|
||
>
|
||
{isPending ? "Загрузка…" : "Показать на карте"}
|
||
</button>
|
||
|
||
{/* Error */}
|
||
{isError && (
|
||
<div
|
||
style={{
|
||
marginTop: 8,
|
||
fontSize: 12,
|
||
color: "#dc2626",
|
||
}}
|
||
>
|
||
{error instanceof Error ? error.message : "Ошибка запроса изохрон"}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|