|
| 1 | +# App Layout + User Profile (Web) |
| 2 | + |
| 3 | +## Description |
| 4 | + |
| 5 | +Implemented the authenticated app shell (layout, header, sidebar) and the `/profile` page on the Nuxt 3 frontend. Also added auto-generated username at registration on the backend, a public profile page at `/u/[username]`, stub pages for reserved/settings, and a custom animated color-mode toggle component. |
| 6 | + |
| 7 | +## Session Log |
| 8 | + |
| 9 | +### Backend — auto-generate username at registration |
| 10 | + |
| 11 | +1. Added `_generate_username_from_email(email)` helper in `apps/api/app/services/auth.py`: |
| 12 | + - Derives a prefix from the email local-part (`re.sub` to keep `[a-z0-9_]`, truncated to 20 chars) |
| 13 | + - Appends a 4-digit random suffix |
| 14 | + - Retries up to 5 times on collision; leaves `username=None` if all collide |
| 15 | + |
| 16 | +2. Modified `register()` to call the helper and assign `username` before `User(...)` creation. |
| 17 | + |
| 18 | +### Frontend — composables |
| 19 | + |
| 20 | +3. Created `apps/web/composables/api/useUsersApi.ts`: |
| 21 | + - `updateProfile(body)` → `PATCH /api/users/me` via `apiFetch`, returns `AuthUser` |
| 22 | + |
| 23 | +4. Created `apps/web/composables/useUsers.ts` (root-level, auto-imported): |
| 24 | + - Wraps `useUsersApi` and calls `store.setUser(updated)` after a successful update |
| 25 | + - Mirrors the `useAuth` → `useAuthApi` pattern; pages never call `useUsersApi` directly |
| 26 | + |
| 27 | +### Frontend — Nuxt layout fix |
| 28 | + |
| 29 | +5. Updated `apps/web/app.vue` to wrap `<NuxtPage>` with `<NuxtLayout>`: |
| 30 | + - Without this, named layouts declared in `definePageMeta` are silently ignored |
| 31 | + |
| 32 | +### Frontend — layouts/app.vue |
| 33 | + |
| 34 | +6. Created `apps/web/layouts/app.vue`: |
| 35 | + - Desktop (`lg+`): sticky `AppHeader` + always-visible `aside` (w-56) + `<slot>` |
| 36 | + - Mobile/tablet (`< lg`): hamburger in header triggers `USlideover` (side="left") containing `AppSidebar` |
| 37 | + - `AppSidebar` emits `navigate` to close the drawer on link click |
| 38 | + |
| 39 | +### Frontend — AppHeader |
| 40 | + |
| 41 | +7. Created `apps/web/components/layout/AppHeader.vue`: |
| 42 | + - Left: hamburger `UButton` (hidden on `lg+`) + logo `NuxtLink` |
| 43 | + - Right: `UiColorModeToggle` + `UDropdownMenu` with `UAvatar` trigger |
| 44 | + - Dropdown: Settings (`/settings`) and Logout (calls `useAuth().logout`) |
| 45 | + |
| 46 | +### Frontend — AppSidebar |
| 47 | + |
| 48 | +8. Created `apps/web/components/layout/AppSidebar.vue`: |
| 49 | + - 5 nav links: Collections (`/dashboard`), Interesting (`/saved`), Profile (`/profile`), Reserved (`/reserved`), Settings (`/settings`) |
| 50 | + - Active state via `useRoute` comparison; emits `navigate` on each click |
| 51 | + |
| 52 | +### Frontend — pages |
| 53 | + |
| 54 | +9. Replaced `apps/web/pages/profile.vue`: |
| 55 | + - `definePageMeta({ layout: 'app', middleware: 'auth', ssr: false })` |
| 56 | + - Editable display name with `isDirty` save button (calls `useUsers().updateProfile`) |
| 57 | + - Read-only `@username`; avatar placeholder |
| 58 | + - Share button: copies `window.location.origin + /u/ + username` to clipboard |
| 59 | + - Stub sections: "My wishlists" and "Wish board" |
| 60 | + |
| 61 | +10. Created `apps/web/pages/u/[username].vue`: |
| 62 | + - Public, no auth, no layout (`definePageMeta({ ssr: false })`) |
| 63 | + - Shows `@username`, stub sections, `useSeoMeta` title |
| 64 | + |
| 65 | +11. Created `apps/web/pages/reserved.vue` and `apps/web/pages/settings.vue` as stubs (`layout: 'app'`, `middleware: 'auth'`) |
| 66 | + |
| 67 | +12. Updated `pages/dashboard.vue` and `pages/saved.vue` to declare `layout: 'app'` |
| 68 | + |
| 69 | +### Frontend — ColorModeToggle |
| 70 | + |
| 71 | +13. Created `apps/web/components/ui/ColorModeToggle.vue`: |
| 72 | + - 2-state cycle: dark ↔ light (system preference treated as dark) |
| 73 | + - Animated icon swap via `<Transition name="wp-spinner" mode="out-in">` reusing existing `wp-spin-in`/`wp-spin-out` keyframes from `main.css` |
| 74 | + - Hover rotation effect (`transform: rotate(18deg)`) |
| 75 | + - `<ClientOnly>` wrapper with static fallback (avoids SSR hydration mismatch) |
| 76 | + - `aria-label` changes with state via i18n keys |
| 77 | + |
| 78 | +14. Replaced `UColorModeButton` in `AppHeader.vue` with `<UiColorModeToggle />` |
| 79 | + |
| 80 | +15. Replaced inline theme toggle block in `pages/index.vue` with `<UiColorModeToggle />`; removed now-unused `colorMode` ref and scoped styles |
| 81 | + |
| 82 | +### Frontend — i18n |
| 83 | + |
| 84 | +16. Added `theme.*` keys (`switch_to_light`, `switch_to_dark`, `switch_to_system`) to both `locales/uk.json` and `locales/en.json` |
| 85 | +17. Added `profile.edit` and `profile.cancel` keys (added by linter pass) |
| 86 | + |
| 87 | +## Session Outcomes |
| 88 | + |
| 89 | +- Authenticated app shell fully working: header + collapsible sidebar + named Nuxt layout |
| 90 | +- `/profile` page: display name edit, share profile link, stub sections |
| 91 | +- `/u/[username]` public profile page |
| 92 | +- Stub pages for `/reserved` and `/settings` |
| 93 | +- Custom animated `UiColorModeToggle` used in both the app header and landing page |
| 94 | +- Auto-generated username on registration |
| 95 | +- `<NuxtLayout>` fix ensures named layouts work across all pages |
| 96 | + |
| 97 | +## Lessons Learned |
| 98 | + |
| 99 | +- Nuxt 3 requires `<NuxtLayout>` in `app.vue`; omitting it causes named layouts to silently not apply — no error, just `<slot>` rendered directly. |
| 100 | +- Nuxt auto-import only scans the root `composables/` directory, not subdirectories like `composables/api/`. Composables in subdirs must be explicitly imported from a root-level wrapper (follow the `useAuth` → `useAuthApi` pattern). |
| 101 | +- Auth-gated pages that use `window`/`navigator` APIs should set `ssr: false` in `definePageMeta` to avoid SSR serialization errors. |
0 commit comments