Add gardens UI: list page + create/edit/delete (#8)
Build image / build-and-push (push) Successful in 13s
Gadfly review (reusable) / review (pull_request) Successful in 7m21s
Adversarial Review (Gadfly) / review (pull_request) Successful in 7m21s

The /gardens page is now home base, backed by the #7 API.

- lib/units.ts: cm <-> meters/feet conversion + formatCm/formatDimensions
  (metric "m", imperial "ft/in"). Minimal for #8; #9's geometry lib
  consolidates.
- lib/gardens.ts: zod gardenSchema + useGardens/useCreateGarden/
  useUpdateGarden/useDeleteGarden (mutations invalidate the list) and
  conflictGarden() to pull the fresh row out of a 409.
- GardensPage: loading/empty/error states; empty state prompts a first
  garden; responsive card grid (single column on phones).
- GardenCard: name, dimensions formatted per the garden's unit, notes
  preview; body links into /gardens/:id, edit/delete kept out of the link.
- GardenFormModal: create/edit with a unit selector; dimensions are entered
  in the chosen unit and converted to cm (switching units converts the
  current values). A 409 rebases the form onto the server's fresh row.
- DeleteGardenModal: confirmation before removing.
- ui/: Modal (Escape/backdrop close), TextArea, Select.

Verified in a real browser against the embedded binary: create appears
without reload; edit persists (version 2), form prefilled from stored cm
converted back to the garden's unit; delete confirms then removes -> empty
state; an imperial 4 ft x 8 ft garden stores 122 x 244 cm and shows
"4' 0" x 8' 0"". tsc --noEmit clean.

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 18:48:47 -04:00
co-authored by Claude Opus 4.8
parent 67c48162c3
commit e0a1522608
9 changed files with 572 additions and 2 deletions
+49
View File
@@ -0,0 +1,49 @@
import { Link } from '@tanstack/react-router'
import type { Garden } from '@/lib/gardens'
import { formatDimensions } from '@/lib/units'
/**
* One garden as a card: the body links into the editor (/gardens/:id); the
* footer has edit/delete actions (kept out of the link so they don't navigate).
*/
export function GardenCard({
garden,
onEdit,
onDelete,
}: {
garden: Garden
onEdit: () => void
onDelete: () => void
}) {
return (
<div className="flex flex-col rounded-xl border border-border bg-surface transition-colors hover:border-accent/50">
<Link
to="/gardens/$gardenId"
params={{ gardenId: String(garden.id) }}
className="flex-1 rounded-t-xl p-4 outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
<h3 className="truncate font-semibold text-fg">{garden.name}</h3>
<p className="mt-1 text-sm text-muted">
{formatDimensions(garden.widthCm, garden.heightCm, garden.unitPref)}
</p>
{garden.notes && <p className="mt-2 line-clamp-2 text-sm text-muted">{garden.notes}</p>}
</Link>
<div className="flex justify-end gap-1 border-t border-border px-2 py-1.5">
<button
type="button"
onClick={onEdit}
className="rounded-md px-2.5 py-1 text-sm font-medium text-muted transition-colors hover:bg-border/50 hover:text-fg"
>
Edit
</button>
<button
type="button"
onClick={onDelete}
className="rounded-md px-2.5 py-1 text-sm font-medium text-muted transition-colors hover:bg-red-500/10 hover:text-red-600 dark:hover:text-red-400"
>
Delete
</button>
</div>
</div>
)
}