import { useEffect, useMemo, useRef, useState } from 'react' import { cn } from '@/lib/cn' import { fieldControlClass } from '@/components/ui/field' import { CategoryChips } from '@/components/plants/CategoryChips' import { PlantIcon } from '@/components/plants/PlantIcon' import { CATEGORY_LABELS, filterPlants, usePlants, type CategoryFilter, type Plant } from '@/lib/plants' import { attributableLots, formatQuantity, lotsByPlant, lotState, summarizeLots, useSeedLots, type SeedLot, } from '@/lib/seedLots' import { LotStateChip } from '@/components/plants/SeedLotList' import { formatSpacing, type UnitPref } from '@/lib/units' const RECENT_KEY = 'pansy:recent-plants' const RECENT_MAX = 8 /** Recently-picked plant ids, most-recent first, from localStorage. */ function loadRecent(): number[] { try { const v: unknown = JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') return Array.isArray(v) ? v.filter((n): n is number => typeof n === 'number') : [] } catch { return [] } } /** Prepend id (dedup, capped) and persist; returns the new list. */ function recordRecent(id: number): number[] { const next = [id, ...loadRecent().filter((n) => n !== id)].slice(0, RECENT_MAX) try { localStorage.setItem(RECENT_KEY, JSON.stringify(next)) } catch { // Recents are a nicety; ignore quota/availability failures. } return next } /** * Reusable plant chooser: a search-first list with a recently-used shortcut and * category filter, rendered as a bottom sheet on mobile / centered panel on * desktop. `onSelect` fires with the chosen plant (and records it as recent). * The plops editor (#15) opens this when placing plants; /plants demos it. */ export function PlantPicker({ onSelect, onClose, unit = 'metric', }: { // The chosen plant, and the lot it should be attributed to when that isn't // ambiguous. Undefined lot means "don't attribute" — which is the honest // answer when there are no lots, and the deliberate one when the user skips. onSelect: (plant: Plant, lot?: SeedLot) => void onClose: () => void unit?: UnitPref }) { const plants = usePlants() const seedLots = useSeedLots() const lotsFor = useMemo(() => lotsByPlant(seedLots.data), [seedLots.data]) // Set when a plant with SEVERAL lots is picked: which packet did this come out // of? One lot auto-attributes and zero lots stays silent, because forcing that // question on every placement is how a nicety becomes an obstacle. const [choosingLotFor, setChoosingLotFor] = useState(null) const [query, setQuery] = useState('') const [category, setCategory] = useState('all') const [recent, setRecent] = useState(() => loadRecent()) const searchRef = useRef(null) const onCloseRef = useRef(onClose) onCloseRef.current = onClose useEffect(() => { searchRef.current?.focus() function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onCloseRef.current() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) }, []) const all = useMemo(() => plants.data ?? [], [plants.data]) const filtered = useMemo(() => filterPlants(all, query, category), [all, query, category]) // Recents show only when not actively searching, and only rows still present. const recentPlants = useMemo(() => { if (query.trim() !== '') return [] const byId = new Map(all.map((p) => [p.id, p])) return recent.map((id) => byId.get(id)).filter((p): p is Plant => !!p) }, [all, recent, query]) function choose(p: Plant) { const lots = attributableLots(lotsFor.get(p.id) ?? []) if (lots.length > 1) { setChoosingLotFor(p) return } setRecent(recordRecent(p.id)) // A single lot attributes even when its count says empty. The count records // what was written down, not a fact about the packet — dropping attribution // there would make a wrong number harder to correct rather than easier. onSelect(p, lots[0]) } function chooseLot(p: Plant, lot?: SeedLot) { setRecent(recordRecent(p.id)) onSelect(p, lot) } // A plant option row. keyPrefix namespaces the key so a plant appearing in // both the Recent and All sections doesn't collide on a duplicate React key. const row = (p: Plant, keyPrefix: string) => ( ) // What's left, shown where you're deciding what to plant. Silent when there // are no lots — most plants won't have any, and "0 left" on all of them would // be noise that trains you to ignore the number. summarizeLots owns the // unit-agreement rule; a second copy of it here would drift. function remainingLabel(lots: SeedLot[]): string { if (lots.length === 0) return '' const { remaining, unit } = summarizeLots(lots) return ` · ${formatQuantity(remaining)}${unit ? ` ${unit}` : ''} left` } return (
e.target === e.currentTarget && onClose()} >
setQuery(e.target.value)} placeholder="Search plants…" aria-label="Search plants" className={cn(fieldControlClass, 'min-w-0 flex-1')} />
{choosingLotFor && (

Which lot of {choosingLotFor.name}?

{attributableLots(lotsFor.get(choosingLotFor.id) ?? []).map((lot) => ( ))}
)}
{plants.isPending &&

Loading plants…

} {plants.isError &&

Couldn't load plants.

} {recentPlants.length > 0 && ( <>

Recent

{recentPlants.map((p) => row(p, 'recent'))}

All plants

)} {filtered.map((p) => row(p, 'all'))} {plants.isSuccess && filtered.length === 0 && (

No plants match your search.

)}
) }