Add plant catalog UI: /plants page + PlantPicker (#13)
Build image / build-and-push (push) Successful in 8s
Gadfly review (reusable) / review (pull_request) Successful in 7m21s
Adversarial Review (Gadfly) / review (pull_request) Successful in 7m21s

- web/src/lib/plants.ts: zod-validated Plant schema + react-query hooks
  (list/create/update/delete) over the #12 endpoints, with the category enum,
  labels, isBuiltin helper, and a 409 conflict extractor.
- /plants page: searchable, category-chip-filtered grid of PlantCard; built-ins
  badged and offer only Duplicate; own plants add Edit/Delete. A local
  metric/imperial spacing toggle (no per-user unit pref exists server-side).
- PlantFormModal: create/edit/duplicate a custom plant (name, category, spacing,
  color, emoji icon, days-to-maturity, notes), unit-aware spacing, 409 rebase.
- DeletePlantModal: confirm + surface the server's PLANT_IN_USE refusal inline.
- editor/PlantPicker.tsx: reusable search-first chooser with recently-used
  (localStorage) and category filter, bottom-sheet on mobile / panel on desktop,
  onSelect callback. Demoed via the /plants "Try the picker" button until #15
  consumes it.
- units.ts: small-scale spacing helpers (cm/inches) + tests.

tsc --noEmit clean; 20/20 vitest; production build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
2026-07-18 21:55:12 -04:00
co-authored by Claude Opus 4.8
parent 293532f021
commit 74d3e3704a
8 changed files with 831 additions and 3 deletions
+26
View File
@@ -68,3 +68,29 @@ export function formatDimensions(widthCm: number, heightCm: number, unit: UnitPr
export function dimensionUnitLabel(unit: UnitPref): string {
return unit === 'imperial' ? 'ft' : 'm'
}
// --- Plant spacing (small scale) -------------------------------------------
// Plant spacing is a handful of centimeters, so meters/feet read poorly here;
// metric shows cm and imperial shows whole inches.
/** Centimeters → a spacing string in the given unit (e.g. "15 cm" or "6″"). */
export function formatSpacing(cm: number, unit: UnitPref): string {
if (unit === 'imperial') return `${Math.round(cm / CM_PER_INCH)}`
return `${Math.round(cm)} cm`
}
/** A spacing value typed in the given unit (cm, or inches) → whole centimeters. */
export function cmFromSpacing(value: number, unit: UnitPref): number {
return unit === 'imperial' ? Math.round(value * CM_PER_INCH) : Math.round(value)
}
/** Centimeters → a spacing number in the given unit, for prefilling an input.
* Inches to 0.1 so cm-quantization doesn't show through. */
export function spacingFromCm(cm: number, unit: UnitPref): number {
return unit === 'imperial' ? Math.round((cm / CM_PER_INCH) * 10) / 10 : Math.round(cm)
}
/** The spacing input-field unit label. */
export function spacingUnitLabel(unit: UnitPref): string {
return unit === 'imperial' ? 'in' : 'cm'
}