fix(ui): F8-04 profile polish — IANA timezone picker, save-stay, shared identity cache
PR-feedback round on #60: - Time-zone field is now a continent-grouped <select> populated from `Intl.supportedValuesOf("timeZone")`, with the browser-detected zone pre-selected when no value is stored. A stored zone the runtime no longer advertises is preserved as an "Other" entry. - Saving the profile no longer kicks the user back to the lobby: the form stays put and shows a transient `saved` notice, cleared on the next edit. Only `cancel` returns to the lobby. - New `lib/account-store.svelte.ts` caches `user.account.get` for the session; lobby + profile share it through `account.ensure()`, so navigating Overview ⇄ Profile no longer flashes the "loading account…" placeholder or fires a second gateway call. Profile save writes through to the store so the shell identity strip picks up the new display name without refetching. Cleared on logout to prevent identity bleed between accounts. - e2e: existing 4 cases adjusted for save-stay; added two new ones for the timezone dropdown and identity-strip stability across navigation. - Docs: `ui/docs/lobby.md` updated to describe the shared cache, the new timezone picker shape, and the save-stay behaviour.
This commit is contained in:
+33
-14
@@ -16,11 +16,23 @@ the right-hand column. The shell uses `var(--font-mono)` so the
|
||||
post-login pages adopt the "nerdy" type stack that the public site
|
||||
already uses.
|
||||
|
||||
The identity strip renders the caller's `display_name` (falling back
|
||||
to the immutable `user_name` handle, then to a loading placeholder
|
||||
while `user.account.get` resolves) as a `data-testid="lobby-account-name"`
|
||||
button. Clicking it switches the top-level screen to `profile`
|
||||
(`appScreen.go("profile")`); the e2e suites use that testid as their
|
||||
The identity strip reads the caller's account from
|
||||
`lib/account-store.svelte.ts` — a session-wide cache that fetches
|
||||
`user.account.get` once on first access and is written through after
|
||||
every Profile save. Both `lobby-screen.svelte` and
|
||||
`profile-screen.svelte` populate the same cache through
|
||||
`account.ensure(client)`, so switching Overview ⇄ Profile never
|
||||
re-issues `user.account.get` and the strip never flashes the
|
||||
`lobby.account_loading` placeholder mid-navigation. The cache is
|
||||
cleared by `session.signOut("user")` / `signOut("revoked")` so a
|
||||
different user signing in on the same browser does not briefly see
|
||||
the previous identity.
|
||||
|
||||
The strip falls back to `display_name` → immutable `user_name` →
|
||||
`lobby.account_loading` while the first `ensure(...)` resolves. It
|
||||
renders as a `data-testid="lobby-account-name"` button; clicking it
|
||||
switches the top-level screen to `profile`
|
||||
(`appScreen.go("profile")`). The e2e suites use that testid as their
|
||||
lobby-loaded signal. The logout button sits next to it
|
||||
(`session.signOut("user")`).
|
||||
|
||||
@@ -49,22 +61,29 @@ same as the create screen — pushing a fresh history entry on entry,
|
||||
falling back to lobby on Back/Forward (see
|
||||
[`navigation.md`](navigation.md)).
|
||||
|
||||
On mount it issues `user.account.get` through `src/api/account.ts`
|
||||
and renders an identity read-out (immutable `user_name`, `email`)
|
||||
plus a three-field form:
|
||||
On mount it reads the caller's account through `account.ensure(...)`
|
||||
(see [Shell](#shell)) — the first visit issues `user.account.get`,
|
||||
subsequent visits resolve from the session-wide cache without a
|
||||
gateway round-trip. The form renders an identity read-out (immutable
|
||||
`user_name`, `email`) plus three editable fields:
|
||||
|
||||
| Field | Endpoint | Notes |
|
||||
| --------------------- | --------------------- | -------------------------------------------------------------- |
|
||||
| `display_name` | `user.profile.update` | Trimmed; empty value clears the stored name (backend PATCH semantics). |
|
||||
| `preferred_language` | `user.settings.update`| `<select>` over `SUPPORTED_LOCALES`; if the stored value is unsupported the option is preserved verbatim so a round-trip save does not silently switch it. |
|
||||
| `time_zone` | `user.settings.update`| Free-text IANA name. Placeholder shows the browser's current zone; backend validates with `time.LoadLocation`. |
|
||||
| `time_zone` | `user.settings.update`| `<select>` of every IANA zone the browser knows (`Intl.supportedValuesOf("timeZone")`), grouped by leading slash segment (Africa / America / …; singletons like `UTC` collapse into a trailing "Other" optgroup). When the form opens with no stored zone, the picker is pre-selected to `Intl.DateTimeFormat().resolvedOptions().timeZone`. A stored value the runtime no longer advertises is added as an extra "Other" entry so the round-trip never silently drops it. Browsers that lack `supportedValuesOf` fall back to a free-text input; the backend validates with `time.LoadLocation` in every shape. |
|
||||
|
||||
Save fires `user.profile.update` and/or `user.settings.update`
|
||||
conditionally on which fields actually changed, then returns to the
|
||||
lobby (`appScreen.go("lobby")`). When the saved
|
||||
`preferred_language` is one the UI also ships translations for, the
|
||||
active i18n locale switches in-place so the rest of the session
|
||||
matches the new preference.
|
||||
conditionally on which fields actually changed, then **stays on the
|
||||
profile** and surfaces a transient `profile-saved-notice` line
|
||||
(`data-testid="profile-saved-notice"`). Editing any field clears the
|
||||
notice. Only the explicit `cancel` button navigates back to the lobby
|
||||
(`appScreen.go("lobby")`). When the saved `preferred_language` is one
|
||||
the UI also ships translations for, the active i18n locale switches
|
||||
in-place so the rest of the session matches the new preference. The
|
||||
write-through is also pushed into the shared `account` store so the
|
||||
shell identity strip picks up the new `display_name` without a second
|
||||
`user.account.get`.
|
||||
|
||||
`GameSummary` carries a `current_turn` field that the lobby UI does
|
||||
not display directly — the in-game shell reads it from the same
|
||||
|
||||
Reference in New Issue
Block a user