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:
+20
-1
@@ -3,11 +3,12 @@
|
||||
// PATCH with the row version; a 409 rolls back to the server's current row and
|
||||
// toasts (see DESIGN § Sync).
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { queryOptions, useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
import { ApiError, api } from './api'
|
||||
import { gardenSchema } from './gardens'
|
||||
import { plantSchema } from './plants'
|
||||
import { plantSchema, type Plant } from './plants'
|
||||
import { serverPlantingSchema, type ServerPlanting } from './plantings'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import type { EditorObject, ObjectShapeKind } from '@/editor/types'
|
||||
@@ -77,6 +78,24 @@ export function useGardenFull(gardenId: number) {
|
||||
return useQuery(gardenFullQueryOptions(gardenId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a plant is present in the /full cache's `plants` list. The full payload
|
||||
* carries only the garden's *referenced* plants (ListReferencedPlants), so a
|
||||
* plant chosen from the full catalog but not yet placed here is absent — its
|
||||
* plops would render without an icon/color until the next refetch. Call this
|
||||
* when arming/placing such a plant so it renders immediately. No-op if present.
|
||||
*/
|
||||
export function useEnsurePlantInFull(gardenId: number) {
|
||||
const qc = useQueryClient()
|
||||
return useCallback(
|
||||
(plant: Plant) =>
|
||||
patchFullCache(qc, gardenId, (full) =>
|
||||
full.plants.some((p) => p.id === plant.id) ? full : { ...full, plants: [...full.plants, plant] },
|
||||
),
|
||||
[qc, gardenId],
|
||||
)
|
||||
}
|
||||
|
||||
// --- mutations -------------------------------------------------------------
|
||||
|
||||
export interface ObjectCreate {
|
||||
|
||||
@@ -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