"use client"; import { useState, useEffect, useRef } from "react"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import type { UserMarker } from "@/components/leaflet-map"; import { TimedUndoAction } from "@/components/time-undo-action"; import { ROAD_STATUSES, LAND_STATUSES, calcLineLength, calcPolygonArea, } from "@/components/land/shape-placement-dialog"; import { LAND_MARKER_LABELS } from "@/lib/map-data"; /* ─── Shared Field component ─────────────────────────────── */ function Field({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) { return (
{label} {children}
); } /* ─── Stat Chip ──────────────────────────────────────────── */ function StatChip({ label, value }: { label: string; value: string }) { return (
{label} {value}
); } /* ─── Status Chip Selector ───────────────────────────────── */ function StatusSelector({ options, value, onChange, }: { options: { value: string; label: string; color: string }[]; value: string; onChange: (v: string) => void; }) { return (
{options.map((opt) => { const isSelected = value === opt.value; return ( ); })}
); } /* ─── Dialog Component ───────────────────────────────────── */ interface ShapeEditDialogProps { open: boolean; onOpenChange: (open: boolean) => void; shape: UserMarker | null; onUpdated: (updated: UserMarker) => void; onDeleted: (deleted: UserMarker) => void; } export function ShapeEditDialog({ open, onOpenChange, shape, onUpdated, onDeleted, }: ShapeEditDialogProps) { const [name, setName] = useState(""); const [status, setStatus] = useState(""); const [notes, setNotes] = useState(""); const DELETE_SECONDS = 5; const TOAST_ID = "shape-delete"; const [deleteActive, setDeleteActive] = useState(false); const [deleteCountdown, setDeleteCountdown] = useState(DELETE_SECONDS); const pendingDeleteShape = useRef(null); const shapeType = shape?.type ?? ""; const isLine = shapeType === "line"; const isPolygon = shapeType === "polygon"; const isCircle = shapeType === "circle"; const titleLabel = `Edit ${LAND_MARKER_LABELS[shapeType] ?? "Item"}`; const coordinates = shape?.meta?.coordinates ?? []; const radius = shape?.meta?.radius ?? 0; const length = isLine ? calcLineLength(coordinates) : 0; const area = isPolygon ? calcPolygonArea(coordinates) : isCircle ? Math.PI * radius * radius : 0; const statusOptions = isLine ? ROAD_STATUSES : isPolygon ? LAND_STATUSES : []; // Reset when different shape opens useEffect(() => { if (!shape?.id) return; setName(shape.name); setStatus(shape.meta?.poi_type ?? (isLine ? "Regency Road" : isPolygon ? "SHM" : "")); setNotes(shape.meta?.notes ?? ""); setDeleteActive(false); setDeleteCountdown(DELETE_SECONDS); pendingDeleteShape.current = null; toast.dismiss(TOAST_ID); // eslint-disable-next-line react-hooks/exhaustive-deps }, [shape?.id]); // Countdown tick useEffect(() => { if (!deleteActive) return; const id = setInterval(() => setDeleteCountdown((p) => Math.max(0, p - 1)), 1000); return () => clearInterval(id); }, [deleteActive]); // Fire delete when countdown hits 0 useEffect(() => { if (!deleteActive || deleteCountdown > 0) return; setDeleteActive(false); setDeleteCountdown(DELETE_SECONDS); toast.dismiss(TOAST_ID); handleDelete(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [deleteActive, deleteCountdown]); // Toast while countdown is running useEffect(() => { if (!deleteActive || !pendingDeleteShape.current) { toast.dismiss(TOAST_ID); return; } const shapeName = pendingDeleteShape.current.name; const pct = (deleteCountdown / DELETE_SECONDS) * 100; toast.custom( () => (

Menghapus shape

"{shapeName}" — dalam {deleteCountdown} detik

), { id: TOAST_ID, duration: Infinity, position: "bottom-left" }, ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [deleteActive, deleteCountdown]); function toggleDelete() { setDeleteActive((prev) => { if (!prev) { pendingDeleteShape.current = shape; setDeleteCountdown(DELETE_SECONDS); } else { pendingDeleteShape.current = null; toast.dismiss(TOAST_ID); } return !prev; }); } function handleDelete() { const target = pendingDeleteShape.current; if (!target) return; pendingDeleteShape.current = null; onDeleted(target); onOpenChange(false); } function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!shape) return; const updated: UserMarker = { ...shape, name: name.trim() || shape.name, meta: { ...shape.meta, poi_type: status || undefined, notes: notes.trim() || undefined, }, }; onUpdated(updated); onOpenChange(false); } return ( { if (!v) onOpenChange(false); }}>
{/* Header */} {titleLabel}

{shape ? `${shape.lat.toFixed(5)}, ${shape.lng.toFixed(5)}` : ""}

{/* Body */}
{/* Name */} setName(e.target.value)} required className="bg-muted/50 border border-border focus-visible:border-ring" /> {/* Notes (optional, all types) */}