feat: initial commit with seeded database and premium ui changes

This commit is contained in:
Dodo
2026-06-11 18:41:42 +07:00
commit edf94ae5c1
1684 changed files with 33073 additions and 0 deletions
@@ -0,0 +1,772 @@
# Religion Dialog Dropdown + Radius Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the religion-POI edit dialog use the shared `dropdown-menu.tsx`
component (matching the create dialog), let users edit a POI's radius, and
fix the missing `radius_meters` DB column so radius circles actually render
on the map.
**Architecture:** Extract the radius slider markup from `religion-dialog.tsx`
into a shared `RadiusSlider` component, reuse it in both the create
(`religion-dialog.tsx`) and edit (`marker-edit-dialog.tsx`) dialogs. Swap
`marker-edit-dialog.tsx`'s religion-type field from the legacy `SelectDropdown`
to the `dropdown-menu.tsx` primitives, expand its religion type list from 3 to
7 entries to match the create dialog, and wire radius load/edit/save through
to `onUpdated` so `leaflet-map.tsx` redraws the circle. Separately, rebuild the
backend Docker container so `initDb()` adds the missing `radius_meters` column
to the live database.
**Tech Stack:** Next.js 16 / React 19 (frontend), Express + `pg` (backend),
Docker Compose, no test runner configured (verification via `eslint` +
`tsc --noEmit` + manual check with the dev server).
---
## Task 1: Extract shared `RadiusSlider` component
**Files:**
- Create: `frontend/components/poverty/radius-slider.tsx`
- [ ] **Step 1: Create the component**
```tsx
"use client";
interface RadiusSliderProps {
value: number;
onChange: (value: number) => void;
}
export function RadiusSlider({ value, onChange }: RadiusSliderProps) {
const pct = (value / 2000) * 100;
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<input
type="range"
min={0}
max={2000}
step={50}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
style={{
background: value === 0
? "var(--border)"
: `linear-gradient(to right, #10b981 0%, #10b981 ${pct}%, var(--border) ${pct}%, var(--border) 100%)`,
}}
className="
flex-1 h-[3px] cursor-pointer appearance-none rounded-full outline-none
[&::-webkit-slider-thumb]:appearance-none
[&::-webkit-slider-thumb]:w-3
[&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full
[&::-webkit-slider-thumb]:bg-white
[&::-webkit-slider-thumb]:shadow
[&::-webkit-slider-thumb]:border
[&::-webkit-slider-thumb]:border-border
[&::-moz-range-thumb]:w-3
[&::-moz-range-thumb]:h-3
[&::-moz-range-thumb]:rounded-full
[&::-moz-range-thumb]:bg-white
[&::-moz-range-thumb]:border
[&::-moz-range-thumb]:border-border
"
/>
<span
className="w-12 shrink-0 text-center text-xs font-bold tabular-nums"
style={{ color: value === 0 ? "var(--muted-foreground)" : "#10b981" }}
>
{value === 0 ? "—" : `${value}m`}
</span>
</div>
<p className="text-[10px] text-muted-foreground">0 = tidak tampilkan lingkaran</p>
</div>
);
}
```
- [ ] **Step 2: Lint check**
Run: `cd /Users/pangkywaradjodi/pba-new/frontend && npx eslint components/poverty/radius-slider.tsx`
Expected: no output, exit code 0.
- [ ] **Step 3: Commit**
```bash
cd /Users/pangkywaradjodi/pba-new/frontend
git add components/poverty/radius-slider.tsx
git commit -m "$(cat <<'EOF'
Extract shared RadiusSlider component
Pulls the radius range-input markup out of religion-dialog.tsx so
marker-edit-dialog.tsx can reuse it for radius editing.
EOF
)"
```
---
## Task 2: Use `RadiusSlider` in `religion-dialog.tsx`
**Files:**
- Modify: `frontend/components/poverty/religion-dialog.tsx`
- [ ] **Step 1: Add the import**
In `frontend/components/poverty/religion-dialog.tsx`, find:
```tsx
import { PhosphorIcon } from "@/components/ui/collection-grid-disclosure";
import { ChevronDownIcon } from "@/components/ui/phosphor-icons";
import { cn } from "@/lib/utils";
import type { UserMarker } from "@/components/leaflet-map";
```
Replace with:
```tsx
import { PhosphorIcon } from "@/components/ui/collection-grid-disclosure";
import { ChevronDownIcon } from "@/components/ui/phosphor-icons";
import { cn } from "@/lib/utils";
import { RadiusSlider } from "@/components/poverty/radius-slider";
import type { UserMarker } from "@/components/leaflet-map";
```
- [ ] **Step 2: Replace the inline slider with `RadiusSlider`**
Find the `Field label="Radius Jangkauan (meter)"` block:
```tsx
<Field label="Radius Jangkauan (meter)">
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<input
type="range"
min={0}
max={2000}
step={50}
value={radiusMeters}
onChange={(e) => setRadiusMeters(Number(e.target.value))}
style={{
background: radiusMeters === 0
? "var(--border)"
: `linear-gradient(to right, #10b981 0%, #10b981 ${(radiusMeters / 2000) * 100}%, var(--border) ${(radiusMeters / 2000) * 100}%, var(--border) 100%)`,
}}
className="
flex-1 h-[3px] cursor-pointer appearance-none rounded-full outline-none
[&::-webkit-slider-thumb]:appearance-none
[&::-webkit-slider-thumb]:w-3
[&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full
[&::-webkit-slider-thumb]:bg-white
[&::-webkit-slider-thumb]:shadow
[&::-webkit-slider-thumb]:border
[&::-webkit-slider-thumb]:border-border
[&::-moz-range-thumb]:w-3
[&::-moz-range-thumb]:h-3
[&::-moz-range-thumb]:rounded-full
[&::-moz-range-thumb]:bg-white
[&::-moz-range-thumb]:border
[&::-moz-range-thumb]:border-border
"
/>
<span className="w-12 shrink-0 text-center text-xs font-bold tabular-nums" style={{ color: radiusMeters === 0 ? "var(--muted-foreground)" : "#10b981" }}>
{radiusMeters === 0 ? "—" : `${radiusMeters}m`}
</span>
</div>
<p className="text-[10px] text-muted-foreground">0 = tidak tampilkan lingkaran</p>
</div>
</Field>
```
Replace with:
```tsx
<Field label="Radius Jangkauan (meter)">
<RadiusSlider value={radiusMeters} onChange={setRadiusMeters} />
</Field>
```
- [ ] **Step 3: Lint check**
Run: `cd /Users/pangkywaradjodi/pba-new/frontend && npx eslint components/poverty/religion-dialog.tsx`
Expected: no output, exit code 0.
- [ ] **Step 4: Commit**
```bash
cd /Users/pangkywaradjodi/pba-new/frontend
git add components/poverty/religion-dialog.tsx
git commit -m "$(cat <<'EOF'
Use shared RadiusSlider in religion-dialog
No behavior change, just dedup ahead of reusing the slider in the
marker edit dialog.
EOF
)"
```
---
## Task 3: Update `marker-edit-dialog.tsx` — dropdown swap, expanded religion list, radius editing
**Files:**
- Modify: `frontend/components/poverty/marker-edit-dialog.tsx`
- [ ] **Step 1: Replace imports**
Find:
```tsx
"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 { Textarea } from "@/components/ui/textarea";
import { SelectDropdown } from "@/components/ui/dropdown";
import type { UserMarker } from "@/components/leaflet-map";
import { TimedUndoAction } from "@/components/time-undo-action";
```
Replace with:
```tsx
"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 { Textarea } from "@/components/ui/textarea";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from "@/components/ui/dropdown-menu";
import { PhosphorIcon } from "@/components/ui/collection-grid-disclosure";
import { ChevronDownIcon } from "@/components/ui/phosphor-icons";
import { cn } from "@/lib/utils";
import { RadiusSlider } from "@/components/poverty/radius-slider";
import type { UserMarker } from "@/components/leaflet-map";
import { TimedUndoAction } from "@/components/time-undo-action";
```
- [ ] **Step 2: Expand `RELIGION_OPTIONS` to 7 entries and extend `MARKER_LABELS`**
Find:
```tsx
const RELIGION_OPTIONS = [
{ value: "mosque", label: "Masjid", icon: "mosque" },
{ value: "church", label: "Gereja", icon: "church" },
{ value: "synagogue", label: "Sinagog", icon: "synagogue" },
];
const MARKER_LABELS: Record<string, string> = {
marker: "Warga Miskin",
mosque: "Masjid",
church: "Gereja",
synagogue: "Sinagog",
clinic: "Klinik",
"food-bank": "Bank Makanan",
school: "Sekolah",
};
```
Replace with:
```tsx
const RELIGION_OPTIONS = [
{ value: "mosque", label: "Masjid (Islam)", icon: "mosque" },
{ value: "church", label: "Gereja (Kristen)", icon: "church" },
{ value: "cathedral", label: "Gereja Katolik", icon: "crown-cross" },
{ value: "temple", label: "Pura (Hindu)", icon: "hands-praying" },
{ value: "vihara", label: "Vihara (Buddha)", icon: "hands-praying" },
{ value: "klenteng", label: "Klenteng (Konghucu)", icon: "hands-praying" },
{ value: "synagogue", label: "Sinagog (Yahudi)", icon: "synagogue" },
];
const MARKER_LABELS: Record<string, string> = {
marker: "Warga Miskin",
mosque: "Masjid",
church: "Gereja",
cathedral: "Gereja Katolik",
temple: "Pura",
vihara: "Vihara",
klenteng: "Klenteng",
synagogue: "Sinagog",
clinic: "Klinik",
"food-bank": "Bank Makanan",
school: "Sekolah",
};
```
This also fixes a side issue: editing a cathedral/temple/vihara/klenteng POI
previously showed no type selector at all because `isReligion` was `false`
for those types (they weren't in the old 3-entry list).
- [ ] **Step 3: Add `radiusMeters` state and `selectedReligion` lookup**
Find:
```tsx
// POI fields
const [placeName, setPlaceName] = useState("");
const [religionType, setReligionType] = useState<string | null>(null);
```
Replace with:
```tsx
// POI fields
const [placeName, setPlaceName] = useState("");
const [religionType, setReligionType] = useState<string | null>(null);
const [radiusMeters, setRadiusMeters] = useState(0);
const selectedReligion = RELIGION_OPTIONS.find((o) => o.value === religionType);
```
- [ ] **Step 4: Load `radius_meters` when the dialog opens**
Find:
```tsx
} else if (isPOI) {
fetch(`${BACKEND_URL}/poi/${dbId}`, { headers })
.then((r) => r.json())
.then((row: any) => {
setPlaceName(row.name ?? "");
setReligionType(row.religion_subtype ?? null);
})
.catch(() => setError("Gagal memuat data"))
.finally(() => setIsLoading(false));
}
```
Replace with:
```tsx
} else if (isPOI) {
fetch(`${BACKEND_URL}/poi/${dbId}`, { headers })
.then((r) => r.json())
.then((row: any) => {
setPlaceName(row.name ?? "");
setReligionType(row.religion_subtype ?? null);
setRadiusMeters(row.radius_meters ?? 0);
})
.catch(() => setError("Gagal memuat data"))
.finally(() => setIsLoading(false));
}
```
- [ ] **Step 5: Send `radius_meters` on save and update `meta.radius` for the map**
Find:
```tsx
} else {
const name = placeName.trim() || (marker.name);
const body: any = { name };
if (isReligion) { body.poi_type = "religion"; body.religion_subtype = religionType; }
const res = await fetch(`${BACKEND_URL}/poi/${dbId}`, {
method: "PUT", headers,
body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? "Gagal menyimpan");
onUpdated({ ...marker, name, ...(isReligion && religionType ? { type: religionType } : {}) });
}
```
Replace with:
```tsx
} else {
const name = placeName.trim() || (marker.name);
const body: any = { name };
if (isReligion) { body.poi_type = "religion"; body.religion_subtype = religionType; body.radius_meters = radiusMeters; }
const res = await fetch(`${BACKEND_URL}/poi/${dbId}`, {
method: "PUT", headers,
body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? "Gagal menyimpan");
onUpdated({
...marker,
name,
...(isReligion && religionType ? { type: religionType } : {}),
...(isReligion ? { meta: { ...marker.meta, radius: radiusMeters } } : {}),
});
}
```
- [ ] **Step 6: Swap the religion-type field to `DropdownMenu` and add the radius field**
Find:
```tsx
) : (
<>
{isReligion && (
<Field label="Jenis Tempat Ibadah">
<SelectDropdown options={RELIGION_OPTIONS} value={religionType} onChange={setReligionType} />
</Field>
)}
<Field label="Nama Tempat">
<Input value={placeName} onChange={(e) => setPlaceName(e.target.value)} className="bg-muted/50 border border-border focus-visible:border-ring" />
</Field>
</>
)}
```
Replace with:
```tsx
) : (
<>
{isReligion && (
<Field label="Jenis Tempat Ibadah">
<DropdownMenu>
<DropdownMenuTrigger
className="flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-muted/50 px-3 py-2 text-sm transition-colors hover:bg-muted focus-visible:border-ring focus-visible:outline-none"
>
<span className="flex min-w-0 items-center gap-2">
{selectedReligion?.icon && (
<span className="size-4 shrink-0 text-foreground">
<PhosphorIcon name={selectedReligion.icon} className="size-4" />
</span>
)}
<span className={cn("truncate", !selectedReligion && "text-muted-foreground")}>
{selectedReligion ? selectedReligion.label : "Pilih jenis…"}
</span>
</span>
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-(--anchor-width)">
<DropdownMenuRadioGroup value={religionType ?? ""} onValueChange={setReligionType}>
{RELIGION_OPTIONS.map((opt) => (
<DropdownMenuRadioItem key={opt.value} value={opt.value}>
<span className="size-4 shrink-0 text-foreground">
<PhosphorIcon name={opt.icon} className="size-4" />
</span>
{opt.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</Field>
)}
<Field label="Nama Tempat">
<Input value={placeName} onChange={(e) => setPlaceName(e.target.value)} className="bg-muted/50 border border-border focus-visible:border-ring" />
</Field>
{isReligion && (
<Field label="Radius Jangkauan (meter)">
<RadiusSlider value={radiusMeters} onChange={setRadiusMeters} />
</Field>
)}
</>
)}
```
- [ ] **Step 7: Lint check**
Run: `cd /Users/pangkywaradjodi/pba-new/frontend && npx eslint components/poverty/marker-edit-dialog.tsx`
Expected: no output, exit code 0.
- [ ] **Step 8: Commit**
```bash
cd /Users/pangkywaradjodi/pba-new/frontend
git add components/poverty/marker-edit-dialog.tsx
git commit -m "$(cat <<'EOF'
Switch marker-edit-dialog to dropdown-menu + add radius editing
- Replace SelectDropdown with the shared dropdown-menu primitives,
matching religion-dialog.tsx.
- Expand religion type options from 3 to 7 (adds cathedral, temple,
vihara, klenteng) so editing those POI types now shows a type
selector at all.
- Load radius_meters when the dialog opens, expose a radius slider
for religion POIs, and send radius_meters on save while updating
meta.radius so the map redraws the circle immediately.
EOF
)"
```
---
## Task 4: Remove the now-unused `SelectDropdown` from `ui/dropdown.tsx`
**Files:**
- Modify: `frontend/components/ui/dropdown.tsx`
This component (and its `SelectOption`/`dropdownSpring` helpers) was only
used by `marker-edit-dialog.tsx`, which Task 3 migrated to `dropdown-menu.tsx`.
- [ ] **Step 1: Confirm no remaining usages**
Run: `cd /Users/pangkywaradjodi/pba-new/frontend && grep -rn "SelectDropdown\|SelectOption" --include="*.tsx" --include="*.ts" . | grep -v node_modules | grep -v .next`
Expected: only matches inside `components/ui/dropdown.tsx` itself.
- [ ] **Step 2: Trim unused imports**
Find:
```tsx
"use client";
import { useState, useEffect, useRef, type FC, type ReactNode } from "react";
import { motion, AnimatePresence, MotionConfig } from "motion/react";
import { ChevronUpIcon } from "lucide-react";
import { PhosphorIcon } from "@/components/ui/collection-grid-disclosure";
import { cn } from "@/lib/utils";
```
Replace with:
```tsx
"use client";
import { useState, useEffect, type FC, type ReactNode } from "react";
import { motion, AnimatePresence, MotionConfig } from "motion/react";
import { ChevronUpIcon } from "lucide-react";
```
- [ ] **Step 3: Remove the `SelectDropdown` section**
Find:
```tsx
// ─── Select Dropdown ────────────────────────────────────────────────────────
export interface SelectOption {
value: string;
label: string;
icon?: string;
}
interface SelectDropdownProps {
options: SelectOption[];
value: string | null;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}
const dropdownSpring = { type: "spring" as const, stiffness: 200, damping: 20, mass: 1.1 };
export const SelectDropdown: FC<SelectDropdownProps> = ({
options,
value,
onChange,
placeholder = "Select…",
className,
}) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [open]);
const selected = options.find((o) => o.value === value);
return (
<MotionConfig transition={dropdownSpring}>
<div ref={ref} className={cn("relative w-full", className)}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center justify-between gap-2 rounded-lg border border-border/60 bg-background/95 px-3 py-2 text-sm shadow-sm backdrop-blur-md transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2 min-w-0">
{selected?.icon && (
<span className="size-4 shrink-0 text-foreground">
<PhosphorIcon name={selected.icon} className="size-4" />
</span>
)}
<span className={cn("truncate", !selected && "text-muted-foreground")}>
{selected ? selected.label : placeholder}
</span>
</div>
<motion.span animate={{ rotate: open ? 180 : 0 }} className="shrink-0 text-muted-foreground">
<ChevronUpIcon className="size-4" />
</motion.span>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -4, scaleY: 0.95 }}
animate={{ opacity: 1, y: 0, scaleY: 1 }}
exit={{ opacity: 0, y: -4, scaleY: 0.95 }}
style={{ originY: 0 }}
className="absolute left-0 right-0 top-full z-50 mt-1 overflow-hidden rounded-lg border border-border/60 bg-background/95 shadow-lg backdrop-blur-md"
>
{options.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => { onChange(opt.value); setOpen(false); }}
className="flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-muted/50"
>
{opt.icon && (
<span className="size-4 shrink-0 text-foreground">
<PhosphorIcon name={opt.icon} className="size-4" />
</span>
)}
<span className="flex-1 text-left">{opt.label}</span>
{value === opt.value && (
<span className="size-3.5 shrink-0 text-primary">
<PhosphorIcon name="check" className="size-3.5" />
</span>
)}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</MotionConfig>
);
};
// ─── Activities Card (existing) ──────────────────────────────────────────────
```
Replace with:
```tsx
// ─── Activities Card (existing) ──────────────────────────────────────────────
```
- [ ] **Step 4: Lint + full type check**
Run: `cd /Users/pangkywaradjodi/pba-new/frontend && npx eslint components/ui/dropdown.tsx components/poverty/marker-edit-dialog.tsx components/poverty/religion-dialog.tsx components/poverty/radius-slider.tsx`
Expected: no output, exit code 0.
Run: `cd /Users/pangkywaradjodi/pba-new/frontend && npx tsc --noEmit`
Expected: no type errors (exit code 0). This catches any leftover references
to `SelectDropdown`/`SelectOption` or mismatched `UserMarker`/`meta` types.
- [ ] **Step 5: Commit**
```bash
cd /Users/pangkywaradjodi/pba-new/frontend
git add components/ui/dropdown.tsx
git commit -m "$(cat <<'EOF'
Remove unused SelectDropdown from ui/dropdown
marker-edit-dialog.tsx was its only consumer and now uses
dropdown-menu.tsx instead.
EOF
)"
```
---
## Task 5: Fix the missing `radius_meters` column on the live database
**Files:** none (infrastructure only — `db-init.js` and `poi.routes.js`
already reference `radius_meters`, the running container is just stale)
- [ ] **Step 1: Confirm the column is currently missing**
Run: `docker exec pba-new-postgres-1 psql -U sig -d sig -c "\d poi_markers"`
Expected (before fix): output does NOT contain a `radius_meters` row.
- [ ] **Step 2: Rebuild and restart the backend container**
Run: `cd /Users/pangkywaradjodi/pba-new && docker compose up -d --build backend`
Expected: build succeeds, `pba-new-backend-1` recreated and running.
- [ ] **Step 3: Verify the column now exists**
Run: `docker exec pba-new-postgres-1 psql -U sig -d sig -c "\d poi_markers"`
Expected: output now contains:
```
radius_meters | integer | | | 0
```
- [ ] **Step 4: Verify the backend is healthy**
Run: `docker logs pba-new-backend-1 --tail 20`
Expected: `Auth server running on port 4000`, no errors.
---
## Task 6: Manual end-to-end verification
**Files:** none (manual QA against the running app)
- [ ] **Step 1: Start the frontend dev server**
Use the `/run` skill (or `cd /Users/pangkywaradjodi/pba-new/frontend && npm run dev`)
to launch the app, then open the poverty dashboard and switch to the Leaflet
map view (satellite or street, doesn't matter).
- [ ] **Step 2: Create a new religion POI with a radius**
Click the map to place a new place of worship. In the "Tambah Tempat Ibadah"
dialog:
- Pick a religion type from the dropdown (confirm it still uses the
dropdown-menu styling/icons as before).
- Set the radius slider to a non-zero value (e.g. 500m).
- Save.
Expected: the marker appears on the map AND a faint dashed circle of the
matching color appears around it with the chosen radius.
- [ ] **Step 3: Edit the POI and verify the new dropdown + radius field**
Click the marker to open the edit dialog. Verify:
- The "Jenis Tempat Ibadah" field now renders using the dropdown-menu
component (same look/feel as the create dialog, with icons), and all 7
religion types are selectable.
- A "Radius Jangkauan (meter)" slider is shown, pre-filled with the radius
set in Step 2.
Change the religion type to a type not in the old 3-option list (e.g.
"Pura (Hindu)" / temple) and change the radius to a different value (e.g.
1000m). Save.
Expected: the marker icon/color updates to match the new type, and the
circle on the map resizes to the new radius.
- [ ] **Step 4: Verify persistence across reload**
Reload the page (re-fetches markers from `/poi`).
Expected: the marker still shows with the updated type, and the radius
circle still renders at the saved radius — confirming `radius_meters` is
now persisted in and read back from the database correctly.
- [ ] **Step 5: Edit radius down to 0**
Open the edit dialog again, set the radius slider to 0, save, reload.
Expected: the marker remains but the circle disappears (per the
"0 = tidak tampilkan lingkaran" convention).
@@ -0,0 +1,83 @@
# Religion Dialog Dropdown + Radius Fix
## Problem
1. The religion-edit flow (`marker-edit-dialog.tsx`) uses an older `SelectDropdown`
(`@/components/ui/dropdown`) for the religion-type field, while the create
flow (`religion-dialog.tsx`) already uses the shared `dropdown-menu.tsx`
primitives. The two dialogs look inconsistent.
2. The edit dialog only offers 3 religion types (mosque/church/synagogue),
while the create dialog offers 7 (+ cathedral, temple, vihara, klenteng).
As a side effect, editing a cathedral/temple/vihara/klenteng POI shows no
type selector at all (`isReligion` is false for those types).
3. The edit dialog has no way to change a religion POI's radius, even though
the create dialog lets you set one.
4. Radius circles never render on the map. Root cause: the live `poi_markers`
table is missing the `radius_meters` column entirely — the running
`pba-new-backend-1` container was built before this column was added to
`db-init.js` / `poi.routes.js`, so radius data never persists.
## Design
### 1. Shared `RadiusSlider` component
New file: `frontend/components/poverty/radius-slider.tsx`.
Extract the slider markup currently inline in `religion-dialog.tsx`
(0-2000m range input, step 50, live value badge, "0 = tidak tampilkan
lingkaran" hint). Props:
```ts
{ value: number; onChange: (v: number) => void }
```
Both dialogs render it inside their own `<Field label="Radius Jangkauan (meter)">`.
### 2. `marker-edit-dialog.tsx`
- Replace `SelectDropdown` with `DropdownMenu` / `DropdownMenuTrigger` /
`DropdownMenuContent` / `DropdownMenuRadioGroup` / `DropdownMenuRadioItem`
from `@/components/ui/dropdown-menu`, mirroring the trigger styling and
icon rendering used in `religion-dialog.tsx`.
- Expand `RELIGION_OPTIONS` to the same 7 entries as `religion-dialog.tsx`
(mosque/church/cathedral/temple/vihara/klenteng/synagogue, with Indonesian
labels and icons).
- Add `radiusMeters` state, populated from `row.radius_meters ?? 0` in the
existing POI-loading `useEffect`.
- Render `<Field label="Radius Jangkauan (meter)"><RadiusSlider .../></Field>`
only when `isReligion`.
- On submit, when `isReligion`: include `radius_meters: radiusMeters` in the
PUT body, and include `radius: radiusMeters` in the `meta` passed to
`onUpdated` so the map redraws the circle immediately.
- Remove the now-dead `SelectDropdown` import.
### 3. `components/ui/dropdown.tsx`
Remove the now-unused `SelectDropdown` / `SelectOption` / `dropdownSpring`
exports (confirmed unused elsewhere via grep).
### 4. `religion-dialog.tsx`
Replace the inline slider block with `<RadiusSlider value={radiusMeters}
onChange={setRadiusMeters} />`. No behavior change, just dedup.
### 5. Backend DB migration
Run `docker compose up -d --build backend` from `/Users/pangkywaradjodi/pba-new`
so `initDb()` re-runs `ALTER TABLE poi_markers ADD COLUMN IF NOT EXISTS
radius_meters INT DEFAULT 0` against the live database. Verify with
`\d poi_markers` afterward.
## Data flow after the fix
- **Create** (`religion-dialog.tsx`): POST `/poi` with `radius_meters` → now
persists correctly once the column exists.
- **Edit** (`marker-edit-dialog.tsx`): GET `/poi/:id` returns `radius_meters`
→ loaded into the slider → PUT `/poi/:id` saves it → `onUpdated` updates
`userMarkers``leaflet-map.tsx` redraws the circle (existing
circle-drawing logic is already correct, it just never received real data).
- **Page load**: GET `/poi` already maps `radius_meters``meta.radius`, so
existing POIs with a radius now render circles too.
## Testing
- Rebuild backend, confirm `radius_meters` column exists.
- Manual flow via dev server: create a religion POI with radius > 0 → circle
appears. Edit an existing religion POI (including a cathedral/temple/etc.),
change type via the new dropdown, adjust radius, save → circle updates.
Reload page → circle persists.