import { useEffect, useMemo, useState } from 'react' import { getRouteApi } from '@tanstack/react-router' import { Alert } from '@/components/ui/Alert' import { Button } from '@/components/ui/Button' import { GardenCanvas } from '@/editor/GardenCanvas' import { Inspector } from '@/editor/Inspector' import { PlopInspector } from '@/editor/PlopInspector' import { PlantPicker } from '@/editor/PlantPicker' import { Palette } from '@/editor/Palette' import { kindDef } from '@/editor/kinds' import { useEditorStore } from '@/editor/store' import type { EditorGarden } from '@/editor/types' import { toEditorObject, useGardenFull, useUpdatePlanting } from '@/lib/objects' import { toEditorPlanting } from '@/lib/plantings' const routeApi = getRouteApi('/gardens/$gardenId') export function GardenEditorPage() { const { gardenId } = routeApi.useParams() const gid = Number(gardenId) const { focus } = routeApi.useSearch() const navigate = routeApi.useNavigate() const full = useGardenFull(gid) const selectedId = useEditorStore((s) => s.selectedId) const select = useEditorStore((s) => s.select) const selectedPlantingId = useEditorStore((s) => s.selectedPlantingId) const selectPlanting = useEditorStore((s) => s.selectPlanting) const focusedObjectId = useEditorStore((s) => s.focusedObjectId) const setFocusedObject = useEditorStore((s) => s.setFocusedObject) const armedPlant = useEditorStore((s) => s.armedPlant) const setArmedPlant = useEditorStore((s) => s.setArmedPlant) const setArmedKind = useEditorStore((s) => s.setArmedKind) const setLiveObject = useEditorStore((s) => s.setLiveObject) const setLivePlanting = useEditorStore((s) => s.setLivePlanting) const updatePlanting = useUpdatePlanting(gid) // Which plant-picker flow is open: 'place' arms a plant for repeat placement; // 'change' swaps the selected plop's plant. const [picker, setPicker] = useState<'place' | 'change' | null>(null) const serverObjects = full.data?.objects const objects = useMemo(() => serverObjects?.map(toEditorObject) ?? [], [serverObjects]) const serverPlantings = full.data?.plantings const plantings = useMemo(() => serverPlantings?.map(toEditorPlanting) ?? [], [serverPlantings]) const plants = useMemo(() => full.data?.plants ?? [], [full.data?.plants]) const plantsById = useMemo(() => new Map(plants.map((p) => [p.id, p])), [plants]) // Adopt the URL's focus and clear transient editor state on entering/switching // gardens (and on leaving). `focus` is intentionally read once here, not a dep, // so URL syncs below don't retrigger a full reset. useEffect(() => { const clear = () => { select(null) selectPlanting(null) setArmedKind(null) setArmedPlant(null) setLiveObject(null) setLivePlanting(null) } clear() setFocusedObject(focus ?? null) return () => { clear() setFocusedObject(null) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [gid]) // Mirror focusedObjectId → ?focus so focus mode is deep-linkable and survives // reload. Declared after the reset effect so the initial adopt wins. useEffect(() => { navigate({ search: (prev) => ({ ...prev, focus: focusedObjectId ?? undefined }), replace: true }) }, [focusedObjectId, navigate]) // If the focused object vanished — deleted, or a stale ?focus id on load — leave // focus mode so the canvas isn't stuck dimmed with no way out. useEffect(() => { if (focusedObjectId != null && full.data && !objects.some((o) => o.id === focusedObjectId)) { setFocusedObject(null) } }, [focusedObjectId, objects, full.data, setFocusedObject]) const exitFocus = () => { setFocusedObject(null) setArmedPlant(null) select(null) selectPlanting(null) } // Escape peels back one layer: stop placing → deselect plop → exit focus → deselect. useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key !== 'Escape') return const s = useEditorStore.getState() if (s.armedPlant) s.setArmedPlant(null) else if (s.selectedPlantingId != null) s.selectPlanting(null) else if (s.focusedObjectId != null) exitFocus() else if (s.selectedId != null) s.select(null) } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) if (full.isPending) return

Loading garden…

if (full.isError) return (
Could not load this garden.
) const g = full.data.garden const garden: EditorGarden = { id: g.id, name: g.name, widthCm: g.widthCm, heightCm: g.heightCm, unitPref: g.unitPref, } const selectedObject = objects.find((o) => o.id === selectedId) ?? null const focusedObject = focusedObjectId != null ? objects.find((o) => o.id === focusedObjectId) ?? null : null // Committed selected plop; PlopInspector applies live drag geometry itself. const selectedPlop = plantings.find((p) => p.id === selectedPlantingId) ?? null function onPickPlant(plantId: number) { const plant = plantsById.get(plantId) if (!plant) return if (picker === 'change' && selectedPlop) { updatePlanting.mutate({ id: selectedPlop.id, version: selectedPlop.version, plantId: plant.id }) } else { setArmedPlant(plant) // 'place' mode: arm for repeat placement } setPicker(null) } return (

{garden.name}

{focusedObjectId == null && }
{focusedObject && (
{focusedObject.name || kindDef(focusedObject.kind)?.label || 'Object'} {!focusedObject.plantable ? ( Not plantable ) : armedPlant ? ( ) : ( )}
)}
{(selectedObject || selectedPlop) && (
{selectedObject && ( { setFocusedObject(selectedObject.id) select(null) }} /> )} {selectedPlop && ( setPicker('change')} onClose={() => selectPlanting(null)} /> )}
)} {picker && ( setPicker(null)} onSelect={(p) => onPickPlant(p.id)} /> )}
) }