Resume the last garden on this device (#97)
Build image / build-and-push (push) Successful in 17s
Gadfly review (reusable) / review (pull_request) Successful in 9m34s
Adversarial Review (Gadfly) / review (pull_request) Successful in 9m34s

`/` 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:
2026-07-22 00:44:58 -04:00
co-authored by Claude Opus 4.8
parent 283010dccb
commit 14af9502d4
5 changed files with 160 additions and 2 deletions
+9
View File
@@ -18,6 +18,7 @@ import { SettingsPage } from '@/pages/SettingsPage'
import { meQueryOptions } from '@/lib/auth'
import { queryClient } from '@/lib/queryClient'
import { safeRedirectPath } from '@/lib/redirect'
import { getLastGardenId } from '@/lib/lastGarden'
interface RouterContext {
queryClient: QueryClient
@@ -66,7 +67,15 @@ async function requireAdmin(context: RouterContext, path: string) {
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
// Resume the garden this device was last in, so a returning user lands back in
// their plot rather than on the list every time. A stored id that no longer
// loads is cleared by the editor and bounces here to /gardens on the next
// visit, so this can't trap anyone on a dead garden.
beforeLoad: () => {
const last = getLastGardenId()
if (last != null) {
throw redirect({ to: '/gardens/$gardenId', params: { gardenId: String(last) } })
}
throw redirect({ to: '/gardens' })
},
})