Fix plant placement + add a Seed Tray (#39) (#42)
Build image / build-and-push (push) Successful in 21s
Build image / build-and-push (push) Successful in 21s
Use the full Plant the picker returns (fixes the silent placement failure) and add a per-garden Seed Tray for quick repeat placing. Review fixes: disarm on tray-remove, cap load, consolidate toolbar guards, rename interface, tidy. Closes #39. Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #42.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// The Seed Tray: a small, deliberately-curated set of plants a gardener keeps at
|
||||
// hand while planting a bed, so repeat placement is a single tap instead of a
|
||||
// trip through the full catalog. It's a per-garden convenience, persisted in
|
||||
// localStorage (same rationale as the PlantPicker's "recent plants"): a nicety,
|
||||
// not authoritative state, so a quota/availability failure is swallowed.
|
||||
//
|
||||
// Only plant ids are stored; they resolve against the live catalog on read, so a
|
||||
// renamed plant stays current and a deleted one silently drops out of the tray.
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { usePlants, type Plant } from './plants'
|
||||
|
||||
const KEY = (gardenId: number) => `pansy:seed-tray:${gardenId}`
|
||||
const TRAY_MAX = 24 // a working set, not a catalog; caps pathological growth
|
||||
|
||||
function loadIds(gardenId: number): number[] {
|
||||
try {
|
||||
const v: unknown = JSON.parse(localStorage.getItem(KEY(gardenId)) ?? '[]')
|
||||
const ids = Array.isArray(v) ? v.filter((n): n is number => typeof n === 'number') : []
|
||||
return ids.slice(-TRAY_MAX) // honor the cap even if storage was hand-edited
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveIds(gardenId: number, ids: number[]) {
|
||||
try {
|
||||
localStorage.setItem(KEY(gardenId), JSON.stringify(ids))
|
||||
} catch {
|
||||
// The tray is a convenience; ignore quota/availability failures.
|
||||
}
|
||||
}
|
||||
|
||||
export interface SeedTrayState {
|
||||
/** Tray plants in insertion order, resolved against the live catalog. */
|
||||
trayPlants: Plant[]
|
||||
/** Add a plant to the end of the tray (no-op if already present; when the tray
|
||||
* is full the oldest entry is evicted). */
|
||||
add: (plant: Plant) => void
|
||||
/** Remove a plant from the tray. */
|
||||
remove: (id: number) => void
|
||||
}
|
||||
|
||||
/** Per-garden Seed Tray backed by localStorage and the plant catalog. */
|
||||
export function useSeedTray(gardenId: number): SeedTrayState {
|
||||
const plants = usePlants()
|
||||
const [ids, setIds] = useState<number[]>(() => loadIds(gardenId))
|
||||
|
||||
// Re-read when switching gardens (the route param changes without remounting).
|
||||
useEffect(() => {
|
||||
setIds(loadIds(gardenId))
|
||||
}, [gardenId])
|
||||
|
||||
const byId = useMemo(() => new Map((plants.data ?? []).map((p) => [p.id, p])), [plants.data])
|
||||
|
||||
// Resolve ids → plants, dropping any no longer in the catalog; preserve order.
|
||||
const trayPlants = useMemo(
|
||||
() => ids.map((id) => byId.get(id)).filter((p): p is Plant => !!p),
|
||||
[ids, byId],
|
||||
)
|
||||
|
||||
const add = useCallback(
|
||||
(plant: Plant) =>
|
||||
setIds((prev) => {
|
||||
if (prev.includes(plant.id)) return prev
|
||||
const next = [...prev, plant.id].slice(-TRAY_MAX)
|
||||
saveIds(gardenId, next)
|
||||
return next
|
||||
}),
|
||||
[gardenId],
|
||||
)
|
||||
|
||||
const remove = useCallback(
|
||||
(id: number) =>
|
||||
setIds((prev) => {
|
||||
const next = prev.filter((x) => x !== id)
|
||||
saveIds(gardenId, next)
|
||||
return next
|
||||
}),
|
||||
[gardenId],
|
||||
)
|
||||
|
||||
return { trayPlants, add, remove }
|
||||
}
|
||||
Reference in New Issue
Block a user