import { useState, type FormEvent } from 'react' import { Alert } from '@/components/ui/Alert' import { Button } from '@/components/ui/Button' import { Dialog } from '@/components/ui/Dialog' import { TextAreaField, TextField } from '@/components/ui/Field' import { Seg } from '@/components/ui/Seg' import { Toggle } from '@/components/ui/Toggle' import { errorMessage } from '@/lib/api' import { conflictGarden, useCreateGarden, useUpdateGarden, type Garden } from '@/lib/gardens' import { cmFromFtIn, convertDimensionField, dimensionField, dimensionInputMode, dimensionUnitLabel, editDimensionField, formatCm, isValidDimensionCm, MIN_GARDEN_GRID_CM, type LengthField, type UnitPref, } from '@/lib/units' // A new plot: the design's 20′ × 12′, spoken in feet; the garden grid defaults // to a foot (the server's 1 m for a metric garden). const DEFAULT_W_FT = 20 const DEFAULT_H_FT = 12 const DEFAULT_GRID_CM = 100 const unitOptions = [ { value: 'imperial' as const, label: 'ft' }, { value: 'metric' as const, label: 'm' }, ] function entryHint(unit: UnitPref): string { return unit === 'imperial' ? `Sizes read as feet and inches — 8' 6", 8', or 8.5 for feet.` : 'Sizes are in meters, e.g. 2.5.' } /** * "A new garden" (no garden) or edit (garden given). Dimensions are typed in the * chosen unit and stored as centimeters: each field is a LengthField, so * switching units re-shows the same centimeters and a Save sends exactly what * was loaded unless the person typed over it (re-parsing the display string is * how 900 cm once became 899.922). A 409 rebases the form onto the server's * fresh row. */ export function GardenDialog({ garden, onClose }: { garden?: Garden; onClose: () => void }) { const isEdit = !!garden const create = useCreateGarden() const update = useUpdateGarden() const pending = create.isPending || update.isPending const initialUnit: UnitPref = garden?.unitPref ?? 'imperial' const [name, setName] = useState(garden?.name ?? '') const [unit, setUnit] = useState(initialUnit) const [width, setWidth] = useState(() => dimensionField(garden?.widthCm ?? cmFromFtIn(DEFAULT_W_FT), initialUnit)) const [height, setHeight] = useState(() => dimensionField(garden?.heightCm ?? cmFromFtIn(DEFAULT_H_FT), initialUnit)) const [gridSize, setGridSize] = useState(() => dimensionField(garden?.gridSizeCm ?? (initialUnit === 'imperial' ? cmFromFtIn(1) : DEFAULT_GRID_CM), initialUnit), ) const [snapToGrid, setSnapToGrid] = useState(garden?.snapToGrid ?? false) const [notes, setNotes] = useState(garden?.notes ?? '') const [more, setMore] = useState(isEdit && (!!garden.notes || garden.snapToGrid)) const [version, setVersion] = useState(garden?.version ?? 0) const [conflict, setConflict] = useState(null) const [formError, setFormError] = useState(null) function changeUnit(next: UnitPref) { setWidth((f) => convertDimensionField(f, next)) setHeight((f) => convertDimensionField(f, next)) setGridSize((f) => convertDimensionField(f, next)) setUnit(next) } const gridSizeCm = gridSize.cm const gridTooFine = gridSizeCm !== null && gridSizeCm > 0 && gridSizeCm < MIN_GARDEN_GRID_CM async function onSubmit(e: FormEvent) { e.preventDefault() setFormError(null) setConflict(null) if (!name.trim()) { setFormError('Give the garden a name.') return } const widthCm = width.cm const heightCm = height.cm if (widthCm === null || heightCm === null) { setFormError(entryHint(unit)) return } if (!isValidDimensionCm(widthCm) || !isValidDimensionCm(heightCm)) { setFormError('Width and depth must be between 1 cm and 100 m.') return } if (gridSizeCm === null || !isValidDimensionCm(gridSizeCm)) { setFormError('The grid must be between 1 cm and 100 m.') return } const input = { name: name.trim(), widthCm, heightCm, unitPref: unit, notes: notes.trim(), gridSizeCm, snapToGrid } // Nothing changed: close without a request. A PATCH that writes the same // row still bumps the version and lands an "Edited garden settings" step // in History that undoes nothing. if (isEdit && (Object.keys(input) as (keyof typeof input)[]).every((k) => input[k] === garden[k])) { onClose() return } try { if (isEdit) await update.mutateAsync({ id: garden.id, ...input, version }) else await create.mutateAsync(input) onClose() } catch (err) { const current = conflictGarden(err) if (current) { setVersion(current.version) setName(current.name) setUnit(current.unitPref) setWidth(dimensionField(current.widthCm, current.unitPref)) setHeight(dimensionField(current.heightCm, current.unitPref)) setGridSize(dimensionField(current.gridSizeCm, current.unitPref)) setSnapToGrid(current.snapToGrid) setNotes(current.notes) setConflict('This garden changed elsewhere. The latest values are shown — look them over and save again.') return } setFormError(errorMessage(err, isEdit ? 'Could not save the garden.' : 'Could not create the garden.')) } } const u = dimensionUnitLabel(unit) const inputMode = dimensionInputMode(unit) return (
{conflict && {conflict}} setName(e.target.value)} />
setWidth(editDimensionField(e.target.value, unit))} wrapperClassName="flex-1" /> setHeight(editDimensionField(e.target.value, unit))} wrapperClassName="flex-1" />
Stored in centimeters under the hood — {unit === 'imperial' ? 'feet are' : 'meters are'} just how you talk.
{!more ? ( ) : (
setGridSize(editDimensionField(e.target.value, unit))} wrapperClassName="flex-1" hint={ gridTooFine && gridSizeCm !== null ? `${formatCm(gridSizeCm, unit)} is very fine for a whole-garden grid — plant spacing lives on each bed.` : undefined } />
setNotes(e.target.value)} />
)} {formError && {formError}}
) }