Build image / build-and-push (push) Successful in 16s
- PublicGardenPage had the same 100vh mobile bug I fixed in the editor — 100dvh there too, so the fix is consistent across both full-height pages. - Cap the toast stack at 4 (drop oldest). Now that error toasts don't auto- dismiss, a burst of failures could otherwise grow the stack unbounded and push the newest — the one that just happened — off-screen. - Hoist the From/To date-input styling to a shared dateInputClass const so the two don't drift. Not taken: reusing safeRedirectPath for the 401 redirect (it validates an incoming redirect param, it doesn't build the outgoing one — the two uses don't overlap), and refactoring the three-branch filter spread (it's clear at three fields; a helper earns its keep when there are more). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
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 isn’t 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(100dvh-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>
|
||
)
|
||
}
|