Feet-and-inches entry: type 2' 7\" instead of 2.6 (#59) (#62)
Build image / build-and-push (push) Successful in 6s
Build image / build-and-push (push) Successful in 6s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #62.
This commit is contained in:
@@ -8,25 +8,34 @@ import { TextField } from '@/components/ui/TextField'
|
||||
import { errorMessage } from '@/lib/api'
|
||||
import { conflictGarden, useCreateGarden, useUpdateGarden, type Garden } from '@/lib/gardens'
|
||||
import {
|
||||
cmFromDisplay,
|
||||
dimensionUnitLabel,
|
||||
displayFromCm,
|
||||
formatCm,
|
||||
dimensionInputMode,
|
||||
formatDimensionInput,
|
||||
isValidDimensionCm,
|
||||
MIN_GARDEN_GRID_CM,
|
||||
parseDimension,
|
||||
type UnitPref,
|
||||
} from '@/lib/units'
|
||||
|
||||
const DEFAULT_METERS = 10 // matches the server's 10 m default
|
||||
const DEFAULT_GRID_CM = 100 // matches the server's 1 m grid default
|
||||
|
||||
// What to say when a field can't be read. Naming the accepted forms beats
|
||||
// "invalid input", which leaves the person guessing which field and which part.
|
||||
function entryHint(unit: UnitPref): string {
|
||||
return unit === 'imperial'
|
||||
? `Enter sizes as feet and inches — 8' 6", 8', or 8.5 for feet.`
|
||||
: 'Enter sizes in meters, e.g. 2.5.'
|
||||
}
|
||||
|
||||
const unitOptions = [
|
||||
{ value: 'metric', label: 'Metric (m)' },
|
||||
{ value: 'imperial', label: 'Imperial (ft)' },
|
||||
]
|
||||
|
||||
function dimString(cm: number | undefined, unit: UnitPref): string {
|
||||
return cm === undefined ? String(DEFAULT_METERS) : String(displayFromCm(cm, unit))
|
||||
return cm === undefined ? String(DEFAULT_METERS) : formatDimensionInput(cm, unit)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +55,7 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
const [width, setWidth] = useState(() => dimString(garden?.widthCm, garden?.unitPref ?? 'metric'))
|
||||
const [height, setHeight] = useState(() => dimString(garden?.heightCm, garden?.unitPref ?? 'metric'))
|
||||
const [gridSize, setGridSize] = useState(() =>
|
||||
String(displayFromCm(garden?.gridSizeCm ?? DEFAULT_GRID_CM, garden?.unitPref ?? 'metric')),
|
||||
formatDimensionInput(garden?.gridSizeCm ?? DEFAULT_GRID_CM, garden?.unitPref ?? 'metric'),
|
||||
)
|
||||
const [snapToGrid, setSnapToGrid] = useState(garden?.snapToGrid ?? false)
|
||||
const [notes, setNotes] = useState(garden?.notes ?? '')
|
||||
@@ -55,9 +64,11 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
|
||||
function changeUnit(next: UnitPref) {
|
||||
// Re-render each field in the new unit, preserving the physical size. An
|
||||
// unparseable field is left as typed rather than blanked.
|
||||
const convert = (s: string) => {
|
||||
const v = parseFloat(s)
|
||||
return Number.isFinite(v) ? String(displayFromCm(cmFromDisplay(v, unit), next)) : s
|
||||
const cm = parseDimension(s, unit)
|
||||
return cm === null ? s : formatDimensionInput(cm, next)
|
||||
}
|
||||
setWidth(convert(width))
|
||||
setHeight(convert(height))
|
||||
@@ -70,11 +81,11 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
|
||||
// Converted once per render and used by both the submit handler and the
|
||||
// too-fine hint, so the two can never disagree about what was entered.
|
||||
const gridSizeCm = cmFromDisplay(parseFloat(gridSize), unit)
|
||||
const gridSizeCm = parseDimension(gridSize, unit)
|
||||
// Soft floor: hint, don't refuse. A garden-scale grid this fine is usually
|
||||
// someone reaching for plant spacing, which lives on the bed instead — but it
|
||||
// is a legitimate choice for a very small garden, so the save still goes through.
|
||||
const gridTooFine = Number.isFinite(gridSizeCm) && gridSizeCm > 0 && gridSizeCm < MIN_GARDEN_GRID_CM
|
||||
const gridTooFine = gridSizeCm !== null && gridSizeCm > 0 && gridSizeCm < MIN_GARDEN_GRID_CM
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
@@ -88,12 +99,20 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
// Validate the converted centimeter values against the same bounds the
|
||||
// server enforces, so sub-cm or over-100m sizes fail here with a clear
|
||||
// message instead of a generic server error.
|
||||
const widthCm = cmFromDisplay(parseFloat(width), unit)
|
||||
const heightCm = cmFromDisplay(parseFloat(height), unit)
|
||||
const widthCm = parseDimension(width, unit)
|
||||
const heightCm = parseDimension(height, unit)
|
||||
if (widthCm === null || heightCm === null) {
|
||||
setFormError(entryHint(unit))
|
||||
return
|
||||
}
|
||||
if (!isValidDimensionCm(widthCm) || !isValidDimensionCm(heightCm)) {
|
||||
setFormError('Width and height must be between 1 cm and 100 m.')
|
||||
return
|
||||
}
|
||||
if (gridSizeCm === null) {
|
||||
setFormError(entryHint(unit))
|
||||
return
|
||||
}
|
||||
if (!isValidDimensionCm(gridSizeCm)) {
|
||||
setFormError('Grid size must be between 1 cm and 100 m.')
|
||||
return
|
||||
@@ -126,7 +145,7 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
setUnit(current.unitPref)
|
||||
setWidth(dimString(current.widthCm, current.unitPref))
|
||||
setHeight(dimString(current.heightCm, current.unitPref))
|
||||
setGridSize(String(displayFromCm(current.gridSizeCm, current.unitPref)))
|
||||
setGridSize(formatDimensionInput(current.gridSizeCm, current.unitPref))
|
||||
setSnapToGrid(current.snapToGrid)
|
||||
setNotes(current.notes)
|
||||
setConflict('This garden changed elsewhere. The latest values are shown — review and save again.')
|
||||
@@ -137,6 +156,7 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
}
|
||||
|
||||
const unitLabel = dimensionUnitLabel(unit)
|
||||
const inputMode = dimensionInputMode(unit)
|
||||
|
||||
return (
|
||||
<Modal title={isEdit ? 'Edit garden' : 'New garden'} onClose={onClose} busy={pending}>
|
||||
@@ -157,10 +177,8 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
<TextField
|
||||
label={`Width (${unitLabel})`}
|
||||
name="width"
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
min="0"
|
||||
type="text"
|
||||
inputMode={inputMode}
|
||||
required
|
||||
value={width}
|
||||
onChange={(e) => setWidth(e.target.value)}
|
||||
@@ -168,10 +186,8 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
<TextField
|
||||
label={`Height (${unitLabel})`}
|
||||
name="height"
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
min="0"
|
||||
type="text"
|
||||
inputMode={inputMode}
|
||||
required
|
||||
value={height}
|
||||
onChange={(e) => setHeight(e.target.value)}
|
||||
@@ -184,10 +200,8 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
||||
<TextField
|
||||
label={`Garden grid (${unitLabel})`}
|
||||
name="gridSize"
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
min="0"
|
||||
type="text"
|
||||
inputMode={inputMode}
|
||||
value={gridSize}
|
||||
onChange={(e) => setGridSize(e.target.value)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user