import { useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/Button' import { TextField } from '@/components/ui/TextField' import { PlantIcon } from '@/components/plants/PlantIcon' import { useRemovePlanting, useUpdatePlanting } from '@/lib/objects' import type { Plant } from '@/lib/plants' import { computeDerivedCount, type EditorPlanting } from '@/lib/plantings' import { cmFromSpacing, spacingFromCm, spacingUnitLabel, type UnitPref } from '@/lib/units' import { MIN_RADIUS_CM } from './shared' import { useEditorStore } from './store' /** * Property panel for the selected plop. Radius/label/date/count commit a PATCH on * blur (carrying the version); the count field shows the live derived value as a * placeholder and takes an override when typed. "Remove" soft-removes (keeps the * row with removed_at); "Change plant" opens the picker via onChangePlant. While * the plop is dragged/resized on the canvas, it reads livePlanting for live * feedback (subscribed here, not at the page level, so a drag only re-renders * this panel). */ export function PlopInspector({ plop, plant, gardenId, unit, onChangePlant, onClose, readOnly = false, }: { plop: EditorPlanting plant?: Plant gardenId: number unit: UnitPref onChangePlant: () => void onClose: () => void readOnly?: boolean }) { const update = useUpdatePlanting(gardenId) const remove = useRemovePlanting(gardenId) const livePlanting = useEditorStore((s) => s.livePlanting) const rootRef = useRef(null) // The plop with any in-flight drag/resize geometry applied. const p = livePlanting && livePlanting.id === plop.id ? livePlanting : plop const [radius, setRadius] = useState(String(spacingFromCm(p.radiusCm, unit))) const [count, setCount] = useState(p.count != null ? String(p.count) : '') const [label, setLabel] = useState(p.label ?? '') const [planted, setPlanted] = useState(p.plantedAt ?? '') // Re-sync when the plop changes underneath us (a drag/resize or a server row), // unless the user is editing a field here. useEffect(() => { if (rootRef.current?.contains(document.activeElement)) return setRadius(String(spacingFromCm(p.radiusCm, unit))) setCount(p.count != null ? String(p.count) : '') setLabel(p.label ?? '') setPlanted(p.plantedAt ?? '') }, [p.version, p.radiusCm, p.count, p.label, p.plantedAt, unit]) const patch = (fields: Omit[0], 'id' | 'version'>) => { if (readOnly) return // a blur mustn't fire a mutation if the role changed mid-edit update.mutate({ id: plop.id, version: plop.version, ...fields }) } const u = spacingUnitLabel(unit) const derived = plant ? computeDerivedCount(p.radiusCm, plant.spacingCm) : p.derivedCount function commitRadius() { const v = Number(radius) if (radius.trim() === '' || !Number.isFinite(v)) { setRadius(String(spacingFromCm(p.radiusCm, unit))) // reset stale/invalid text return } const cm = Math.max(MIN_RADIUS_CM, cmFromSpacing(v, unit)) if (cm !== p.radiusCm) patch({ radiusCm: cm }) } function commitCount() { if (count.trim() === '') { if (p.count != null) patch({ count: null }) // restore derived return } const n = Number(count) if (!Number.isInteger(n) || n < 1) { setCount(p.count != null ? String(p.count) : '') // reject invalid, restore return } if (n !== p.count) patch({ count: n }) } function commitPlanted() { const next = planted === '' ? null : planted if (next !== (p.plantedAt ?? null)) patch({ plantedAt: next }) } return (

Plant

{readOnly && (

View only — you can't edit this garden.

)}
{plant ? ( ) : ( ? )} {plant?.name ?? 'Unknown plant'} {!readOnly && ( )}
setRadius(e.target.value)} onBlur={commitRadius} /> setCount(e.target.value)} onBlur={commitCount} hint={p.count != null ? 'Override — clear for auto' : 'Auto from area ÷ spacing²'} />
setLabel(e.target.value)} onBlur={() => label !== (p.label ?? '') && patch({ label: label.trim() || null })} /> setPlanted(e.target.value)} onBlur={commitPlanted} />
{!readOnly && ( )}
) }