Seed shelf UI: source links, lots, and what's left (#51) (#68)
Build image / build-and-push (push) Successful in 11s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #68.
This commit is contained in:
2026-07-21 06:03:12 +00:00
committed by steve
parent 7f8b5254d0
commit 5d9b10d7c7
15 changed files with 1067 additions and 11 deletions
@@ -16,6 +16,7 @@ import {
type PlantCategory,
type PlantInput,
} from '@/lib/plants'
import { safeExternalUrl } from '@/lib/seedLots'
import { cmFromSpacing, spacingFromCm, spacingUnitLabel, type UnitPref } from '@/lib/units'
const DEFAULT_COLOR = '#4a7c3f'
@@ -61,6 +62,8 @@ export function PlantFormModal({
const [color, setColor] = useState(expandHex(source?.color ?? DEFAULT_COLOR))
const [icon, setIcon] = useState(source?.icon ?? DEFAULT_ICON)
const [days, setDays] = useState(source?.daysToMaturity != null ? String(source.daysToMaturity) : '')
const [sourceUrl, setSourceUrl] = useState(source?.sourceUrl ?? '')
const [vendor, setVendor] = useState(source?.vendor ?? '')
const [notes, setNotes] = useState(source?.notes ?? '')
const [version, setVersion] = useState(plant?.version ?? 0)
const [conflict, setConflict] = useState<string | null>(null)
@@ -96,6 +99,14 @@ export function PlantFormModal({
daysToMaturity = d
}
// The server refuses anything that isn't http(s) with a host, but say so here
// rather than letting a paste of "johnnyseeds.com" come back as a generic
// error with no hint about which field or why.
if (sourceUrl.trim() && !safeExternalUrl(sourceUrl.trim())) {
setFormError('The source link needs to be a full http:// or https:// address.')
return
}
const input: PlantInput = {
name: name.trim(),
category,
@@ -103,6 +114,8 @@ export function PlantFormModal({
color,
icon: icon.trim(),
daysToMaturity,
sourceUrl: sourceUrl.trim(),
vendor: vendor.trim(),
notes: notes.trim(),
}
@@ -194,6 +207,27 @@ export function PlantFormModal({
onChange={(e) => setDays(e.target.value)}
/>
{/* Provenance for the variety itself. What you bought and what's left
of it is a seed lot, added from the plant card. */}
<div className="grid grid-cols-2 gap-3">
<TextField
label="Vendor"
name="vendor"
placeholder="Johnny's Selected Seeds"
value={vendor}
onChange={(e) => setVendor(e.target.value)}
/>
<TextField
label="Source link"
name="sourceUrl"
type="url"
inputMode="url"
placeholder="https://…"
value={sourceUrl}
onChange={(e) => setSourceUrl(e.target.value)}
/>
</div>
<TextArea label="Notes" name="notes" rows={2} value={notes} onChange={(e) => setNotes(e.target.value)} />
{formError && <Alert>{formError}</Alert>}