Add gardens UI: list page + create/edit/delete (#8)
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:
@@ -1,5 +1,58 @@
|
||||
import { PageStub } from '@/components/PageStub'
|
||||
import { useState } from 'react'
|
||||
import { Alert } from '@/components/ui/Alert'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { DeleteGardenModal } from '@/components/gardens/DeleteGardenModal'
|
||||
import { GardenCard } from '@/components/gardens/GardenCard'
|
||||
import { GardenFormModal } from '@/components/gardens/GardenFormModal'
|
||||
import { useGardens, type Garden } from '@/lib/gardens'
|
||||
|
||||
// Which modal is open, if any. `edit`/`delete` carry the target garden.
|
||||
type Dialog = { kind: 'create' } | { kind: 'edit'; garden: Garden } | { kind: 'delete'; garden: Garden } | null
|
||||
|
||||
export function GardensPage() {
|
||||
return <PageStub title="Gardens">The garden list and create flow arrive in issue #8.</PageStub>
|
||||
const gardens = useGardens()
|
||||
const [dialog, setDialog] = useState<Dialog>(null)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Gardens</h1>
|
||||
{gardens.data && gardens.data.length > 0 && (
|
||||
<Button onClick={() => setDialog({ kind: 'create' })}>New garden</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
{gardens.isPending && <p className="text-sm text-muted">Loading your gardens…</p>}
|
||||
{gardens.isError && <Alert>Could not load your gardens. Please refresh.</Alert>}
|
||||
|
||||
{gardens.isSuccess && gardens.data.length === 0 && (
|
||||
<div className="rounded-xl border border-dashed border-border p-10 text-center">
|
||||
<p className="text-fg">No gardens yet.</p>
|
||||
<p className="mt-1 text-sm text-muted">Create your first garden to start planning.</p>
|
||||
<Button className="mt-4" onClick={() => setDialog({ kind: 'create' })}>
|
||||
Create your first garden
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{gardens.isSuccess && gardens.data.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{gardens.data.map((g) => (
|
||||
<GardenCard
|
||||
key={g.id}
|
||||
garden={g}
|
||||
onEdit={() => setDialog({ kind: 'edit', garden: g })}
|
||||
onDelete={() => setDialog({ kind: 'delete', garden: g })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dialog?.kind === 'create' && <GardenFormModal onClose={() => setDialog(null)} />}
|
||||
{dialog?.kind === 'edit' && <GardenFormModal garden={dialog.garden} onClose={() => setDialog(null)} />}
|
||||
{dialog?.kind === 'delete' && <DeleteGardenModal garden={dialog.garden} onClose={() => setDialog(null)} />}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user