Resume the last garden on this device (#97)
`/` always dumped you on the gardens list; a phone user who lives in one garden had to open the list and tap in every time. Now the device remembers the garden it was last in and `/` resumes there. - lib/lastGarden.ts: per-device localStorage (pansy:last-garden), same swallow-failures rationale as the seed tray / recents. getLastGardenId guards against a non-positive/garbage stored value. - The `/` route redirects to the stored garden when present, else /gardens. - The editor records the garden on successful load, and — if it 404s (deleted or access revoked) — forgets it (only if it's the stored one, so a bad direct link can't wipe a good resume target) and bounces to the list, so a stale id can't trap the user on an error screen. Transient errors still show the retryable message. - ApiError.isNotFound getter (mirrors isConflict/isUnauthorized). Verified live at 390px: resume into the last garden; a stale id bounces to /gardens and clears itself. tsc + vitest (incl. new lastGarden test) green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
@@ -36,6 +36,8 @@ import { useJournalCounts } from '@/lib/journal'
|
||||
import { attributableLots, lotsByPlant, useSeedLots, type SeedLot } from '@/lib/seedLots'
|
||||
import { useSeedTray } from '@/lib/seedTray'
|
||||
import { usePageTitle } from '@/lib/usePageTitle'
|
||||
import { rememberLastGarden, forgetLastGarden } from '@/lib/lastGarden'
|
||||
import { ApiError } from '@/lib/api'
|
||||
|
||||
const routeApi = getRouteApi('/gardens/$gardenId')
|
||||
|
||||
@@ -133,6 +135,24 @@ export function GardenEditorPage() {
|
||||
navigate({ search: (prev) => ({ ...prev, focus: focusedObjectId ?? undefined }), replace: true })
|
||||
}, [focusedObjectId, navigate])
|
||||
|
||||
// Remember this garden as the device's last-opened, so `/` resumes here next
|
||||
// visit. Keyed on the live query: which garden EXISTS doesn't depend on the
|
||||
// season being viewed.
|
||||
useEffect(() => {
|
||||
if (live.isSuccess) rememberLastGarden(gid)
|
||||
}, [live.isSuccess, gid])
|
||||
|
||||
// A last garden that 404s (deleted, or access revoked) must not strand the user
|
||||
// on an error screen the `/` resume keeps returning to: forget it (only if it's
|
||||
// this one, so a bad direct link can't wipe a good resume target) and bounce to
|
||||
// the list. Transient errors (500/network) fall through to the retryable screen.
|
||||
useEffect(() => {
|
||||
if (live.isError && live.error instanceof ApiError && live.error.isNotFound) {
|
||||
forgetLastGarden(gid)
|
||||
navigate({ to: '/gardens' })
|
||||
}
|
||||
}, [live.isError, live.error, gid, 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(() => {
|
||||
@@ -256,12 +276,20 @@ export function GardenEditorPage() {
|
||||
}, [])
|
||||
|
||||
if (full.isPending) return <p className="p-6 text-sm text-muted">Loading garden…</p>
|
||||
if (full.isError)
|
||||
if (full.isError) {
|
||||
// A not-found is handled by the effect above (forget + bounce to the list);
|
||||
// say so rather than flashing a dead-end error during the redirect.
|
||||
const gone = full.error instanceof ApiError && full.error.isNotFound
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Alert>Could not load this garden.</Alert>
|
||||
<Alert>
|
||||
{gone
|
||||
? 'That garden is no longer available — taking you to your gardens…'
|
||||
: 'Could not load this garden.'}
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const g = full.data.garden
|
||||
const garden: EditorGarden = {
|
||||
|
||||
Reference in New Issue
Block a user