Files
pansy/web/src/pages/PublicGardenPage.tsx
T
steveandClaude Opus 4.8 3f4d5627d8
Build image / build-and-push (push) Successful in 15s
Gadfly review (reusable) / review (pull_request) Successful in 10m8s
Adversarial Review (Gadfly) / review (pull_request) Successful in 10m9s
Add configurable grid + snap to the layout system
Two independent grids, each with a size and a snap toggle:

- Garden grid (owner-set, in the garden settings form): drives the
  editor's visual grid and, when snapping is on, snaps objects on
  place/move and snaps their dimensions to whole grid steps on resize.
- Bed grid (per plantable object, in the bed Inspector): when snapping
  is on, plants snap to the bed's grid on place/move, and the grid is
  drawn inside the focused bed. Plant radius stays free.

Snapping defaults off on both, so existing gardens keep free placement.
Grid spacing shares the [1cm, 100m] range and defaults to 1m (garden) /
30cm (bed); 0 is treated as unset and re-defaulted.

Backend: migration 0005 adds grid_size_cm/snap_to_grid to gardens and
garden_objects, threaded through domain/store/service/api with service +
api round-trip tests. Frontend: new geometry snap helpers (unit-tested),
schema/type plumbing, canvas + overlay snapping, and the two forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
2026-07-19 02:51:44 -04:00

75 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo } from 'react'
import { getRouteApi } from '@tanstack/react-router'
import { Alert } from '@/components/ui/Alert'
import { GardenCanvas } from '@/editor/GardenCanvas'
import { useEditorStore } from '@/editor/store'
import type { EditorGarden } from '@/editor/types'
import { toEditorObject } from '@/lib/objects'
import { toEditorPlanting } from '@/lib/plantings'
import { usePublicGarden } from '@/lib/publicGarden'
import { usePageTitle } from '@/lib/usePageTitle'
const routeApi = getRouteApi('/g/$token')
/**
* The public, read-only garden view behind a share token. Rendered on the
* unauthenticated `/g/$token` route (no auth guard), so a logged-out visitor
* never hits /login or OIDC. Reuses GardenCanvas with canEdit=false — pan/zoom
* to explore, but no editing chrome.
*/
export function PublicGardenPage() {
const { token } = routeApi.useParams()
const full = usePublicGarden(token)
usePageTitle(full.data?.garden.name ?? 'Shared garden')
// Start from a clean canvas: if a logged-in user reaches here from their own
// editor, stale focus/selection state would otherwise dim or highlight things.
useEffect(() => {
useEditorStore.getState().resetTransient()
}, [token])
const objects = useMemo(() => full.data?.objects.map(toEditorObject) ?? [], [full.data?.objects])
const plantings = useMemo(() => full.data?.plantings.map(toEditorPlanting) ?? [], [full.data?.plantings])
const plants = useMemo(() => full.data?.plants ?? [], [full.data?.plants])
const plantsById = useMemo(() => new Map(plants.map((p) => [p.id, p])), [plants])
if (full.isPending) return <p className="p-6 text-sm text-muted">Loading garden</p>
if (full.isError)
return (
<div className="p-6">
<Alert>This shared link isnt available. The owner may have turned it off or changed it.</Alert>
</div>
)
const g = full.data.garden
const garden: EditorGarden = {
id: g.id,
name: g.name,
widthCm: g.widthCm,
heightCm: g.heightCm,
unitPref: g.unitPref,
gridSizeCm: g.gridSizeCm,
snapToGrid: g.snapToGrid,
}
return (
<div className="flex h-[calc(100vh-8rem)] flex-col gap-3">
<div className="flex flex-wrap items-center gap-2">
<h1 className="truncate text-lg font-semibold tracking-tight" title={garden.name}>
{garden.name}
</h1>
<span className="rounded-md bg-border/40 px-2 py-1 text-xs text-muted">👁 Shared · read-only</span>
</div>
<div className="min-h-0 flex-1">
<GardenCanvas
garden={garden}
objects={objects}
plantings={plantings}
plantsById={plantsById}
canEdit={false}
/>
</div>
</div>
)
}