Files
q-blog/docs/features/TECH_IMPL_MULTIBLOG.md
greenflame089 0b100af686 Release v0.2.2
2025-08-22 07:28:42 -04:00

216 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Guide — Multiple Blogs per Name (Plan A)
_Generated 2025-08-21_
> **AAA mode:** Actionable, artifact-oriented, and accessible. This guide defines exact files to create or modify, contracts, a11y rules, redirects, and tests.
---
## Contracts & Routing
### Entities (unchanged)
- **Name**: can own multiple **Blog** records.
- **Blog**: immutable link to its **Name**; addressed by `blogHandle` (aka blog id).
- **Post**: 1..1 Blog (unchanged).
### Routes (final form)
- `/{name}`
- If **hasExactlyOneBlog(name)** → redirect (replace history) to `/{name}/{blog}/posts` (or current posts index).
- Else → route to **User Blogs** page (`/{name}/blogs`).
- `/{name}/blogs`**User Blogs** page (new).
- `/{name}/{blog}/*`**existing blog-scoped routes** (unchanged).
**Assumption:** Existing default blog landing is `/{name}/{blog}/posts`. Adjust if your index differs.
---
## UI Changes
### 1) Header/Nav — “My Blogs” dropdown
**File:** `src/components/HeaderNav.tsx` (or equivalent container for auth controls)
- Replace single “My Blog”/“Create Blog” button with a **dropdown** (same pattern as the Authenticate/Names dropdown).
- **State inputs:**
- `currentUser?: NameRef`
- `userBlogs: BlogSummary[]` — list of blogs for current user (title + `blogHandle`).
- `onCreateBlog: () => void`
- `onSelectBlog: (blog: BlogRef) => void` → navigates to `/{name}/{blog}/posts` and updates active blog in store.
- **Label logic:**
- `userBlogs.length === 0` → show **Create Blog** (primary button), hide dropdown caret.
- `userBlogs.length >= 1` → show **My Blogs** with caret; open menu on click.
- **Menu content (for ≥1):**
- One item per blog: `Title · blogHandle` (truncate cautiously). Current active shows a ✓.
- Divider
- **Create new blog** (button-like menu item).
- **A11y:**
- Use proper `role="menu"`/`menuitem"`, `aria-haspopup="menu"`, `aria-expanded`, `aria-controls`.
- Focus returns to the trigger on close. ESC closes.
- Ensure high-contrast tokens; respect `prefers-reduced-motion` (no aggressive animation).
> **Note:** If a reusable `BlogSwitcher` exists, consider refactoring the above as a `UserBlogSwitcher` variant that consumes the current users blogs.
### 2) User Blogs page (new)
**File:** `src/pages/UserBlogs.tsx` (or `src/routes/UserBlogs.tsx`)
- **H1:** “{name}s Blogs”
- **List:** Card per blog with title, `blogHandle`, optional description/meta.
- **Actions:**
- View → pushes route `/{name}/{blog}/posts`.
- If `currentUser.nameId === params.nameId` → buttons: **Edit**, **Create Blog** (page-level CTA).
- **Empty states:**
- Self (0 blogs): “You dont have any blogs yet.” + prominent **Create Blog**.
- Other user (0 blogs): “No public blogs yet.”
**A11y:** One H1, list semantics, buttons with explicit labels (include blog title in `aria-label`).
---
## Store & Data
### Selectors
- `selectCurrentUser()` → NameRef / undefined.
- `selectUserBlogs(nameId)` → BlogSummary[] (RTK Query / slice selector).
- `selectActiveBlog()` → BlogRef | null (unchanged).
### RTK Query (if missing)
- **Endpoint:** `listBlogsByName(nameId)`
- Cache key: `['blogs', nameId]`.
- Normalized response: `BlogSummary[]` (include `blogHandle`, `title`).
- Consumers:
- Header dropdown fetches `listBlogsByName(currentUser.nameId)` when authenticated.
- `UserBlogs` page fetches `listBlogsByName(params.nameId)`.
> If an endpoint already exists, reuse it. Otherwise add it in `src/store/api.ts` with blog-scoped cache keys documented in Q-Blog.
---
## Routing Changes
**File:** `src/router.tsx` (or where routes are defined)
1. Add route for `/{name}/blogs``UserBlogs` component.
2. Update `/{name}` route element to perform **smart redirect**:
- On mount:
- Fetch `blogs = listBlogsByName(params.nameId)`.
- If `blogs.length === 1``navigate('/{name}/' + blogs[0].blogHandle + '/posts', { replace: true })`.
- Else → `navigate('/{name}/blogs', { replace: true })`.
- **Loading state:** show a lightweight spinner/skeleton with polite live region (“Loading blogs…”).
> Keep deep links to `/{name}/{blog}/…` working as today.
---
## Permissions
- Editing controls on `UserBlogs` are visible only when viewing your own Name (`currentUser.nameId === params.nameId`).
- All blog/post routes retain existing guards (no change).
---
## Edge Cases & Behavior
- **0 blogs (self):** Header shows **Create Blog**. `/{name}` goes to `/blogs` with the “create” empty state.
- **1 blog:** Header shows **My Blogs**; menu has one item + “Create new”. Name root redirects to that blog.
- **N blogs (large):** Menu scrolls; consider collapsed ids or optional search later (not part of this change).
- **Anonymous viewer:** Can see another users `/{name}/blogs`, no edit/create controls.
- **Broken blogHandle:** If a listed blog 404s, show error boundary as today; menu item should not break the app.
---
## Accessibility Requirements
- Dropdown follows WAI-ARIA menu pattern; keyboard: ArrowUp/Down, Enter, ESC.
- Focus management: opening menu moves focus to first item; closing returns to trigger.
- Single H1 on `UserBlogs`. Buttons have informative `aria-label`s (“View blog Alpha (alpha)”).
- Live region only for the brief loading text on Name root redirect; no chatty updates.
---
## Testing Plan (Vitest + RTL + user-event + MSW + jest-axe)
> If MSW/jest-axe arent present yet, include unit tests now and add MSW/a11y smoke later. Aim to cover behavior, not visuals.
1. **Header/Menu**
- 0 blogs → shows “Create Blog” button, no dropdown.
- 1+ blogs → shows “My Blogs”; opens menu; selecting a blog navigates to proper route and sets active blog.
- Keyboard navigation: items reachable via arrows; ESC closes; focus returns to trigger.
2. **Name Root Redirect**
- 1 blog → navigates to blog posts route (replace history).
- 0 or >1 → navigates to `/{name}/blogs`.
- Loading state renders politely with an `aria-live="polite"` region text.
3. **UserBlogs Page**
- Renders list matching `listBlogsByName` response.
- Self-view shows Edit + Create; other-view hides them.
- Empty states render correct CTAs/messages.
4. **A11y/axe Smoke**
- Header and `UserBlogs` produce no critical axe violations.
---
## Step-by-step Changes (files)
1. **Create:** `src/pages/UserBlogs.tsx`
- Functional component rendering list of blogs by `params.nameId`.
- Uses RTK Query selector/hook to fetch `listBlogsByName`.
- Buttons: View, (conditional) Edit; page-level Create when self.
2. **Modify:** `src/router.tsx`
- Add `/{name}/blogs` route.
- Update `/{name}` route element to perform smart redirect based on fetched blog count.
3. **Modify:** `src/components/HeaderNav.tsx`
- Replace single button with dropdown:
- When 0 blogs → primary **Create Blog** button.
- When ≥1 blogs → **My Blogs** dropdown listing blog items + “Create new blog”.
- Wire `onSelectBlog` to navigate and set active blog; wire `onCreateBlog` to current flow (dialog/route).
4. **Add (if missing):** `src/store/api.ts`
- Endpoint: `listBlogsByName(nameId)` with `providesTags: (nameId) => ['blogs', nameId]` or equivalent.
- Types: `BlogSummary` (title, blogHandle).
5. **Tests:**
- `tests/routes/UserBlogs.test.tsx`
- `tests/components/HeaderNav.multiblog.test.tsx`
- `tests/routes/NameRootRedirect.test.tsx`
6. **Docs:**
- Link this guide from `docs/USER_JOURNEYS.md` and `docs/ARCHITECTURE.md` after merge.
---
## Acceptance Criteria
- Header shows **Create Blog** when user has 0 blogs; shows **My Blogs** dropdown when ≥1.
- Selecting a blog from the dropdown navigates to `/{name}/{blog}/posts` and marks it active.
- Visiting `/{name}`:
- If exactly one blog exists → silently lands on that blogs posts list.
- Else → shows `/{name}/blogs` page listing all blogs for that Name.
- `UserBlogs` page renders with a single H1, accessible list, and correct empty states.
- Unit/component tests pass; a11y smoke has no critical issues.
- No regressions to existing blog-scoped routes; deep links remain valid.
---
## Rollout Notes
- No data migrations; purely UI/routing.
- If telemetry exists, consider logging menu usage and redirect outcomes to validate adoption.
- Update release notes after merge.