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 (