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:
@@ -0,0 +1,40 @@
|
||||
import { forwardRef, useId, type SelectHTMLAttributes } from 'react'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label: string
|
||||
options: { value: string; label: string }[]
|
||||
}
|
||||
|
||||
// Labelled native <select>, styled to match the text inputs.
|
||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
|
||||
{ label, options, id, name, className, ...props },
|
||||
ref,
|
||||
) {
|
||||
const generatedId = useId()
|
||||
const fieldId = id ?? name ?? generatedId
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor={fieldId} className="text-sm font-medium text-fg">
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
ref={ref}
|
||||
id={fieldId}
|
||||
name={name}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-surface px-3 py-2 text-base text-fg',
|
||||
'outline-none transition-colors focus:border-accent focus:ring-2 focus:ring-accent/30 disabled:opacity-60',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user