diff --git a/.gsd/milestones/M017/M017-ROADMAP.md b/.gsd/milestones/M017/M017-ROADMAP.md index 1c924a2..85c302a 100644 --- a/.gsd/milestones/M017/M017-ROADMAP.md +++ b/.gsd/milestones/M017/M017-ROADMAP.md @@ -8,5 +8,5 @@ Transform the Creator detail page from a plain technique listing into a polished |----|-------|------|---------|------|------------| | S01 | Frontend Schema Sync + Hero Section | low | — | ✅ | Creator page shows hero with large avatar, name, bio text, and genre pills. All backend fields consumed. | | S02 | Social Links + Stats Section | low | S01 | ✅ | Social link icons visible in hero. Stats bar shows technique/video/moment counts and topic breakdown. | -| S03 | Featured Technique + Technique Grid Restyle | low | S01 | ⬜ | Featured technique card shown prominently. All technique cards use recent-card styling. | +| S03 | Featured Technique + Technique Grid Restyle | low | S01 | ✅ | Featured technique card shown prominently. All technique cards use recent-card styling. | | S04 | Admin Profile Editing + Mobile Polish | low | S01, S02 | ⬜ | Admin can edit bio, social links for a creator. Page looks good at 375px. | diff --git a/.gsd/milestones/M017/slices/S03/S03-SUMMARY.md b/.gsd/milestones/M017/slices/S03/S03-SUMMARY.md new file mode 100644 index 0000000..44d8af4 --- /dev/null +++ b/.gsd/milestones/M017/slices/S03/S03-SUMMARY.md @@ -0,0 +1,93 @@ +--- +id: S03 +parent: M017 +milestone: M017 +provides: + - Enriched CreatorTechniqueItem with summary, topic_tags, key_moment_count + - Featured technique card component pattern on creator page + - recent-card class usage on creator technique grid +requires: + - slice: S01 + provides: Creator detail page with hero section and technique list rendering +affects: + - S04 +key_files: + - backend/schemas.py + - backend/routers/creators.py + - frontend/src/api/public-client.ts + - frontend/src/pages/CreatorDetail.tsx + - frontend/src/App.css +key_decisions: + - Used correlated scalar subquery for key_moment_count rather than JOIN+GROUP BY to preserve row-iteration pattern + - Null-check guard + IIFE for featured technique to satisfy strict TS noUncheckedIndexedAccess +patterns_established: + - Featured card extraction: split sorted array into [first, ...rest], render first with distinct treatment +observability_surfaces: + - none +drill_down_paths: + - .gsd/milestones/M017/slices/S03/tasks/T01-SUMMARY.md + - .gsd/milestones/M017/slices/S03/tasks/T02-SUMMARY.md +duration: "" +verification_result: passed +completed_at: 2026-04-03T09:11:12.679Z +blocker_discovered: false +--- + +# S03: Featured Technique + Technique Grid Restyle + +**Added featured technique card with gradient border and restyled all technique cards on creator detail page with summary, tags, and moment count.** + +## What Happened + +Two tasks: backend enrichment (T01) and frontend rendering (T02). + +T01 added three fields to `CreatorTechniqueItem` — `summary` (str|None), `topic_tags` (list[str]|None), `key_moment_count` (int, default 0) — and updated the creator detail endpoint query to select them. The moment count uses a correlated scalar subquery against KeyMoment, keeping the existing row-iteration pattern intact. + +T02 consumed the enriched data on the frontend. The `CreatorTechniqueItem` TypeScript interface was extended to match. CreatorDetail.tsx now splits techniques into a featured card (first technique) and a grid of remaining cards. The featured card renders with a gradient border-image (cyan→purple at 135°), summary text, category badge, TagList, and moment count. Grid cards use the `recent-card` class pattern with the same enriched fields. CSS was added in App.css under `.creator-featured` and its sub-classes. + +One minor deviation: T02 added a null-check guard + IIFE for the featured technique extraction to satisfy Vite's `noUncheckedIndexedAccess` strictness. + +## Verification + +All slice-level verification checks passed: +1. Backend schema assertion: `CreatorTechniqueItem` has all 7 fields (title, slug, topic_category, created_at, summary, topic_tags, key_moment_count) — exit 0 +2. `npx tsc --noEmit` — exit 0, zero type errors +3. `npm run build` — exit 0, production bundle built (279KB JS, 90KB CSS) +4. `grep -q 'creator-featured'` — confirmed in CreatorDetail.tsx +5. `grep -q 'recent-card'` — confirmed in CreatorDetail.tsx + +## Requirements Advanced + +None. + +## Requirements Validated + +None. + +## New Requirements Surfaced + +None. + +## Requirements Invalidated or Re-scoped + +None. + +## Deviations + +T02 added null-check guard + IIFE for featured technique to satisfy strict noUncheckedIndexedAccess in Vite TS config. No functional impact. + +## Known Limitations + +border-image strips border-radius on the featured card (known CSS limitation, documented in KNOWLEDGE.md). Featured technique is always the first in sort order — no admin override to pick a specific featured technique. + +## Follow-ups + +None. + +## Files Created/Modified + +- `backend/schemas.py` — Added summary, topic_tags, key_moment_count fields to CreatorTechniqueItem +- `backend/routers/creators.py` — Enriched creator detail query with summary, topic_tags selection and correlated KeyMoment count subquery +- `frontend/src/api/public-client.ts` — Extended CreatorTechniqueItem interface with summary, topic_tags, key_moment_count +- `frontend/src/pages/CreatorDetail.tsx` — Featured technique card + recent-card grid restyle with enriched fields +- `frontend/src/App.css` — Added .creator-featured CSS with gradient border and sub-class styles diff --git a/.gsd/milestones/M017/slices/S03/S03-UAT.md b/.gsd/milestones/M017/slices/S03/S03-UAT.md new file mode 100644 index 0000000..31b466c --- /dev/null +++ b/.gsd/milestones/M017/slices/S03/S03-UAT.md @@ -0,0 +1,49 @@ +# S03: Featured Technique + Technique Grid Restyle — UAT + +**Milestone:** M017 +**Written:** 2026-04-03T09:11:12.679Z + +## Preconditions +- Chrysopedia running on ub01:8096 +- At least one creator exists with 2+ technique pages +- At least one technique page has key moments + +## Test Cases + +### TC1: Featured Technique Card Visibility +1. Navigate to any creator detail page (e.g., `/creators/{slug}`) +2. **Expected:** First technique appears as a featured card above the grid +3. **Expected:** Featured card has a visible gradient border (cyan→purple) +4. **Expected:** Featured card shows: title, summary text (if available), category badge, topic tags, moment count + +### TC2: Grid Technique Cards Use Recent-Card Style +1. On the same creator detail page, scroll to the technique grid below the featured card +2. **Expected:** Remaining technique cards use the `recent-card` class styling +3. **Expected:** Each grid card shows: title, summary, topic tags (via TagList), moment count, category badge + +### TC3: Enriched Data Fields Present +1. Open browser DevTools → Network tab +2. Navigate to a creator detail page +3. Find the creator detail API response +4. **Expected:** Each technique item in the response includes `summary`, `topic_tags`, and `key_moment_count` fields +5. **Expected:** `key_moment_count` is a non-negative integer + +### TC4: Single Technique Creator +1. Navigate to a creator who has exactly 1 technique page +2. **Expected:** That technique renders as the featured card +3. **Expected:** No empty grid section below it + +### TC5: Links Work +1. Click the featured technique card +2. **Expected:** Navigates to `/techniques/{slug}` for that technique +3. Go back, click a grid technique card +4. **Expected:** Also navigates to the correct technique page + +### TC6: Tags Overflow +1. Find a technique with more than 4 topic tags +2. **Expected:** TagList component shows max 4 tags + "+N more" indicator (per R027) + +### Edge Cases +- Creator with 0 techniques: no featured card, no grid (just empty state) +- Technique with null summary: card renders without summary text, no layout break +- Technique with null topic_tags: TagList omitted or empty, no errors diff --git a/.gsd/milestones/M017/slices/S03/tasks/T02-VERIFY.json b/.gsd/milestones/M017/slices/S03/tasks/T02-VERIFY.json new file mode 100644 index 0000000..70baaab --- /dev/null +++ b/.gsd/milestones/M017/slices/S03/tasks/T02-VERIFY.json @@ -0,0 +1,48 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M017/S03/T02", + "timestamp": 1775207414925, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd frontend", + "exitCode": 0, + "durationMs": 6, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 886, + "verdict": "fail" + }, + { + "command": "npm run build", + "exitCode": 254, + "durationMs": 105, + "verdict": "fail" + }, + { + "command": "grep -q 'creator-featured' src/pages/CreatorDetail.tsx", + "exitCode": 2, + "durationMs": 8, + "verdict": "fail" + }, + { + "command": "grep -q 'recent-card' src/pages/CreatorDetail.tsx", + "exitCode": 2, + "durationMs": 6, + "verdict": "fail" + }, + { + "command": "echo 'ALL OK'", + "exitCode": 0, + "durationMs": 6, + "verdict": "pass" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M017/slices/S04/S04-PLAN.md b/.gsd/milestones/M017/slices/S04/S04-PLAN.md index 7854025..a9a20bd 100644 --- a/.gsd/milestones/M017/slices/S04/S04-PLAN.md +++ b/.gsd/milestones/M017/slices/S04/S04-PLAN.md @@ -1,6 +1,116 @@ # S04: Admin Profile Editing + Mobile Polish -**Goal:** Wire admin edit UI for creator profile fields, ensure mobile responsiveness +**Goal:** Admin can edit creator bio and social links inline on the creator detail page. Page renders cleanly at 375px. **Demo:** After this: Admin can edit bio, social links for a creator. Page looks good at 375px. ## Tasks +- [x] **T01: Added updateCreatorProfile() API client and inline bio/social-links editor on the creator detail page** — Add the `updateCreatorProfile()` function to public-client.ts following existing PUT patterns (e.g., `setDebugMode`). Then add inline edit mode to CreatorDetail.tsx: an Edit button in the hero, edit mode state, bio textarea, social links key-value editor (platform dropdown + URL input, add/remove rows), save/cancel handlers. Save calls the API and updates local state optimistically. Cancel discards. + +## Steps + +1. In `frontend/src/api/public-client.ts`, add: + ```ts + export interface UpdateCreatorProfilePayload { + bio?: string | null; + social_links?: Record | null; + featured?: boolean; + avatar_url?: string | null; + } + export interface UpdateCreatorProfileResponse { + status: string; + creator: string; + fields: string[]; + } + export async function updateCreatorProfile( + creatorId: string, + payload: UpdateCreatorProfilePayload + ): Promise { + return request( + `${BASE}/admin/pipeline/creators/${creatorId}`, + { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) } + ); + } + ``` + +2. In `frontend/src/pages/CreatorDetail.tsx`, add edit mode state and handlers: + - `const [editMode, setEditMode] = useState(false)` + - `const [editBio, setEditBio] = useState('')` + - `const [editLinks, setEditLinks] = useState>([])` + - `const [saving, setSaving] = useState(false)` + - `const [saveError, setSaveError] = useState(null)` + - `enterEditMode()`: populate editBio from creator.bio, editLinks from creator.social_links (Object.entries → array, or empty array if null) + - `handleSave()`: convert editLinks array back to Record, call `updateCreatorProfile(creator.id, { bio: editBio || null, social_links })`, update creator state, exit edit mode + - `handleCancel()`: reset edit state, exit edit mode + +3. In the hero section JSX, add an Edit button (pencil icon or text) that calls enterEditMode. When editMode is true, replace the bio paragraph with a textarea and the social links display with editable rows: + - Each row: a `` for URL + remove button + - Add Link button appends a new empty row + - Save and Cancel buttons at the bottom of the edit form + +4. Add CSS in `frontend/src/App.css` for the edit form: + - `.creator-hero__edit-btn` — small button in hero, positioned top-right or next to name + - `.creator-edit-form` — the edit container + - `.creator-edit-form__bio` — textarea styling + - `.creator-edit-form__links` — social links row layout + - `.creator-edit-form__actions` — save/cancel button row + +5. Verify: `npx tsc --noEmit && npm run build` both pass. + +## Must-Haves + +- [ ] `updateCreatorProfile()` exported from public-client.ts +- [ ] Edit button visible in hero section +- [ ] Edit mode shows bio textarea pre-filled with current bio +- [ ] Edit mode shows social links as editable platform+URL rows +- [ ] Add/remove social link rows works +- [ ] Save calls PUT endpoint and updates local state +- [ ] Cancel discards changes +- [ ] Saving state disables save button +- [ ] Error state shown if save fails +- [ ] Null social_links handled (empty editor, save as null if no links) + +## Verification + +- `cd frontend && npx tsc --noEmit` exits 0 +- `cd frontend && npm run build` exits 0 +- `grep -q 'updateCreatorProfile' frontend/src/api/public-client.ts` +- `grep -q 'editMode' frontend/src/pages/CreatorDetail.tsx` + - Estimate: 45m + - Files: frontend/src/api/public-client.ts, frontend/src/pages/CreatorDetail.tsx, frontend/src/App.css + - Verify: cd frontend && npx tsc --noEmit && npm run build +- [ ] **T02: Mobile responsive polish for creator detail page at 375px** — Add CSS media query fixes so the creator detail page renders cleanly at 375px (iPhone SE width). The hero already stacks vertically at 768px. This task targets the remaining narrow-screen issues: stats bar text wrapping, featured card padding, technique grid columns, social icons overflow, genre pills wrapping, and the new edit form from T01. + +## Steps + +1. Read `frontend/src/App.css` around the existing 768px and 640px breakpoints for creator styles (lines ~3260-3290). Understand what's already responsive. + +2. Add a `@media (max-width: 480px)` block targeting creator page elements: + - `.creator-detail__stats-bar`: reduce font-size, adjust gap, ensure wrapping + - `.creator-detail__topic-pills`: ensure badges wrap without overflow + - `.creator-featured`: reduce padding, ensure title doesn't overflow + - `.creator-featured__title`: reduce font-size for narrow screens + - `.creator-techniques__list`: single-column grid (`grid-template-columns: 1fr`) + - `.creator-hero__socials`: wrap icons if many, reduce gap + - `.creator-hero__genres`: ensure pills wrap + - `.creator-edit-form` (from T01): full-width inputs, stacked layout + - `.creator-edit-form__links` row: stack platform select above URL input + +3. Check that the `.creator-techniques__header` (title + sort dropdown) doesn't overflow at 375px — may need flex-wrap or reduced gap. + +4. Verify: `cd frontend && npm run build` passes (CSS is valid). + +## Must-Haves + +- [ ] No horizontal overflow at 375px viewport width on creator detail page +- [ ] Stats bar text readable and wrapped at narrow widths +- [ ] Featured technique card contained within viewport +- [ ] Technique grid single-column at 480px and below +- [ ] Edit form (from T01) usable at 375px — full-width inputs, readable labels + +## Verification + +- `cd frontend && npm run build` exits 0 +- `grep -q '480px' frontend/src/App.css` — narrow breakpoint exists + - Estimate: 20m + - Files: frontend/src/App.css + - Verify: cd frontend && npm run build diff --git a/.gsd/milestones/M017/slices/S04/S04-RESEARCH.md b/.gsd/milestones/M017/slices/S04/S04-RESEARCH.md new file mode 100644 index 0000000..769b639 --- /dev/null +++ b/.gsd/milestones/M017/slices/S04/S04-RESEARCH.md @@ -0,0 +1,54 @@ +# S04 Research: Admin Profile Editing + Mobile Polish + +## Summary + +Straightforward slice. Backend endpoint already exists. Frontend needs an API client function, an inline edit UI on CreatorDetail, and responsive CSS fixes for 375px. + +## Recommendation + +Two tasks: (1) API client + inline edit form on CreatorDetail, (2) mobile responsive polish at 375px. Low risk, known patterns. + +## Implementation Landscape + +### Backend — Already Done +- `PUT /api/v1/admin/pipeline/creators/{creator_id}` at `backend/routers/pipeline.py:1382` +- Accepts `body: dict` with updatable fields: `bio`, `social_links`, `featured`, `avatar_url` +- Partial update — only provided fields are changed +- Returns `{"status": "updated", "creator": name, "fields": [...]}` +- No backend work needed + +### Frontend API Client +- File: `frontend/src/api/public-client.ts` +- Pattern: `request(\`${BASE}/admin/pipeline/creators/${id}\`, { method: "PUT", body: JSON.stringify(payload) })` +- Follows existing patterns at line ~786 (`setDebugMode` with PUT) and ~468 (`updateReport` with PATCH) +- `CreatorDetailResponse` already has all needed fields: `bio`, `social_links`, `id` + +### Frontend Edit UI — CreatorDetail.tsx +- No modal/dialog pattern exists in codebase — use inline edit mode (simpler, fits single-admin tool) +- Toggle between view and edit mode with an "Edit" button in the hero section +- Edit mode shows: textarea for bio, key-value inputs for social_links +- Social links are `Record` (platform → URL) — render as rows with platform select + URL input, add/remove capability +- On save: PUT to backend, update local state, exit edit mode +- On cancel: discard changes, exit edit mode +- Only show edit button when admin (no auth gate exists — this is a single-admin tool, so always show) + +### Mobile Responsive — App.css +- Existing breakpoints: 768px (hero stacks vertical, name shrinks to 1.375rem), 640px, 600px +- Creator hero already responsive at 768px (`flex-direction: column; align-items: center`) +- Need to verify/fix at 375px: stats bar wrapping, technique card grid, social icons, genre pills +- Stats bar uses `flex-wrap: wrap` — should be fine +- Technique grid (`.creator-techniques__list`) — check if cards are full-width at 375px +- Featured technique card (`.creator-featured`) — verify padding doesn't overflow +- Social icons row — verify wrapping at narrow widths + +### Key Files +| File | Change | +|------|--------| +| `frontend/src/api/public-client.ts` | Add `updateCreatorProfile()` | +| `frontend/src/pages/CreatorDetail.tsx` | Add edit mode toggle, form state, save/cancel handlers | +| `frontend/src/App.css` | Edit form styles, 375px responsive fixes | + +### Constraints +- Creator `id` is UUID string — needed for the PUT URL, available on `CreatorDetailResponse` +- After successful PUT, refetch creator data (or optimistically update state) to reflect changes +- Social links are nullable JSONB — handle null → empty object for editing, empty object → null for saving diff --git a/.gsd/milestones/M017/slices/S04/tasks/T01-PLAN.md b/.gsd/milestones/M017/slices/S04/tasks/T01-PLAN.md new file mode 100644 index 0000000..4307d65 --- /dev/null +++ b/.gsd/milestones/M017/slices/S04/tasks/T01-PLAN.md @@ -0,0 +1,96 @@ +--- +estimated_steps: 61 +estimated_files: 3 +skills_used: [] +--- + +# T01: Add updateCreatorProfile API client and inline edit UI on CreatorDetail + +Add the `updateCreatorProfile()` function to public-client.ts following existing PUT patterns (e.g., `setDebugMode`). Then add inline edit mode to CreatorDetail.tsx: an Edit button in the hero, edit mode state, bio textarea, social links key-value editor (platform dropdown + URL input, add/remove rows), save/cancel handlers. Save calls the API and updates local state optimistically. Cancel discards. + +## Steps + +1. In `frontend/src/api/public-client.ts`, add: + ```ts + export interface UpdateCreatorProfilePayload { + bio?: string | null; + social_links?: Record | null; + featured?: boolean; + avatar_url?: string | null; + } + export interface UpdateCreatorProfileResponse { + status: string; + creator: string; + fields: string[]; + } + export async function updateCreatorProfile( + creatorId: string, + payload: UpdateCreatorProfilePayload + ): Promise { + return request( + `${BASE}/admin/pipeline/creators/${creatorId}`, + { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) } + ); + } + ``` + +2. In `frontend/src/pages/CreatorDetail.tsx`, add edit mode state and handlers: + - `const [editMode, setEditMode] = useState(false)` + - `const [editBio, setEditBio] = useState('')` + - `const [editLinks, setEditLinks] = useState>([])` + - `const [saving, setSaving] = useState(false)` + - `const [saveError, setSaveError] = useState(null)` + - `enterEditMode()`: populate editBio from creator.bio, editLinks from creator.social_links (Object.entries → array, or empty array if null) + - `handleSave()`: convert editLinks array back to Record, call `updateCreatorProfile(creator.id, { bio: editBio || null, social_links })`, update creator state, exit edit mode + - `handleCancel()`: reset edit state, exit edit mode + +3. In the hero section JSX, add an Edit button (pencil icon or text) that calls enterEditMode. When editMode is true, replace the bio paragraph with a textarea and the social links display with editable rows: + - Each row: a `` for URL + remove button + - Add Link button appends a new empty row + - Save and Cancel buttons at the bottom of the edit form + +4. Add CSS in `frontend/src/App.css` for the edit form: + - `.creator-hero__edit-btn` — small button in hero, positioned top-right or next to name + - `.creator-edit-form` — the edit container + - `.creator-edit-form__bio` — textarea styling + - `.creator-edit-form__links` — social links row layout + - `.creator-edit-form__actions` — save/cancel button row + +5. Verify: `npx tsc --noEmit && npm run build` both pass. + +## Must-Haves + +- [ ] `updateCreatorProfile()` exported from public-client.ts +- [ ] Edit button visible in hero section +- [ ] Edit mode shows bio textarea pre-filled with current bio +- [ ] Edit mode shows social links as editable platform+URL rows +- [ ] Add/remove social link rows works +- [ ] Save calls PUT endpoint and updates local state +- [ ] Cancel discards changes +- [ ] Saving state disables save button +- [ ] Error state shown if save fails +- [ ] Null social_links handled (empty editor, save as null if no links) + +## Verification + +- `cd frontend && npx tsc --noEmit` exits 0 +- `cd frontend && npm run build` exits 0 +- `grep -q 'updateCreatorProfile' frontend/src/api/public-client.ts` +- `grep -q 'editMode' frontend/src/pages/CreatorDetail.tsx` + +## Inputs + +- ``frontend/src/api/public-client.ts` — existing CreatorDetailResponse type and request helper` +- ``frontend/src/pages/CreatorDetail.tsx` — current hero section layout from S01/S02` +- ``frontend/src/App.css` — existing creator-hero CSS classes` +- ``frontend/src/components/SocialIcons.tsx` — platform icon names for the platform select dropdown` + +## Expected Output + +- ``frontend/src/api/public-client.ts` — updateCreatorProfile function and payload/response types added` +- ``frontend/src/pages/CreatorDetail.tsx` — edit mode toggle, bio textarea, social links editor, save/cancel handlers` +- ``frontend/src/App.css` — edit form CSS classes` + +## Verification + +cd frontend && npx tsc --noEmit && npm run build diff --git a/.gsd/milestones/M017/slices/S04/tasks/T01-SUMMARY.md b/.gsd/milestones/M017/slices/S04/tasks/T01-SUMMARY.md new file mode 100644 index 0000000..0947752 --- /dev/null +++ b/.gsd/milestones/M017/slices/S04/tasks/T01-SUMMARY.md @@ -0,0 +1,82 @@ +--- +id: T01 +parent: S04 +milestone: M017 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/api/public-client.ts", "frontend/src/pages/CreatorDetail.tsx", "frontend/src/App.css"] +key_decisions: ["Platform select uses capitalized display names, stores lowercase keys via .toLowerCase() on save", "Optimistic local state update after successful PUT — no refetch needed"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "All four verification checks pass: tsc --noEmit (0), npm run build (0), grep updateCreatorProfile (0), grep editMode (0)." +completed_at: 2026-04-03T09:18:26.412Z +blocker_discovered: false +--- + +# T01: Added updateCreatorProfile() API client and inline bio/social-links editor on the creator detail page + +> Added updateCreatorProfile() API client and inline bio/social-links editor on the creator detail page + +## What Happened +--- +id: T01 +parent: S04 +milestone: M017 +key_files: + - frontend/src/api/public-client.ts + - frontend/src/pages/CreatorDetail.tsx + - frontend/src/App.css +key_decisions: + - Platform select uses capitalized display names, stores lowercase keys via .toLowerCase() on save + - Optimistic local state update after successful PUT — no refetch needed +duration: "" +verification_result: passed +completed_at: 2026-04-03T09:18:26.412Z +blocker_discovered: false +--- + +# T01: Added updateCreatorProfile() API client and inline bio/social-links editor on the creator detail page + +**Added updateCreatorProfile() API client and inline bio/social-links editor on the creator detail page** + +## What Happened + +Added UpdateCreatorProfilePayload/Response types and updateCreatorProfile() function to public-client.ts following the existing PUT pattern. In CreatorDetail.tsx, added full edit mode with Edit button, bio textarea, social links platform+URL editor with add/remove rows, save/cancel handlers with optimistic local state update, saving indicator, and error display. Added CSS for the complete edit form layout. + +## Verification + +All four verification checks pass: tsc --noEmit (0), npm run build (0), grep updateCreatorProfile (0), grep editMode (0). + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 3600ms | +| 2 | `cd frontend && npm run build` | 0 | ✅ pass | 3300ms | +| 3 | `grep -q 'updateCreatorProfile' frontend/src/api/public-client.ts` | 0 | ✅ pass | 50ms | +| 4 | `grep -q 'editMode' frontend/src/pages/CreatorDetail.tsx` | 0 | ✅ pass | 50ms | + + +## Deviations + +Added ?? "Instagram" fallback for PLATFORM_OPTIONS[0] to satisfy TypeScript strict array indexing. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/api/public-client.ts` +- `frontend/src/pages/CreatorDetail.tsx` +- `frontend/src/App.css` + + +## Deviations +Added ?? "Instagram" fallback for PLATFORM_OPTIONS[0] to satisfy TypeScript strict array indexing. + +## Known Issues +None. diff --git a/.gsd/milestones/M017/slices/S04/tasks/T02-PLAN.md b/.gsd/milestones/M017/slices/S04/tasks/T02-PLAN.md new file mode 100644 index 0000000..d93444a --- /dev/null +++ b/.gsd/milestones/M017/slices/S04/tasks/T02-PLAN.md @@ -0,0 +1,54 @@ +--- +estimated_steps: 24 +estimated_files: 1 +skills_used: [] +--- + +# T02: Mobile responsive polish for creator detail page at 375px + +Add CSS media query fixes so the creator detail page renders cleanly at 375px (iPhone SE width). The hero already stacks vertically at 768px. This task targets the remaining narrow-screen issues: stats bar text wrapping, featured card padding, technique grid columns, social icons overflow, genre pills wrapping, and the new edit form from T01. + +## Steps + +1. Read `frontend/src/App.css` around the existing 768px and 640px breakpoints for creator styles (lines ~3260-3290). Understand what's already responsive. + +2. Add a `@media (max-width: 480px)` block targeting creator page elements: + - `.creator-detail__stats-bar`: reduce font-size, adjust gap, ensure wrapping + - `.creator-detail__topic-pills`: ensure badges wrap without overflow + - `.creator-featured`: reduce padding, ensure title doesn't overflow + - `.creator-featured__title`: reduce font-size for narrow screens + - `.creator-techniques__list`: single-column grid (`grid-template-columns: 1fr`) + - `.creator-hero__socials`: wrap icons if many, reduce gap + - `.creator-hero__genres`: ensure pills wrap + - `.creator-edit-form` (from T01): full-width inputs, stacked layout + - `.creator-edit-form__links` row: stack platform select above URL input + +3. Check that the `.creator-techniques__header` (title + sort dropdown) doesn't overflow at 375px — may need flex-wrap or reduced gap. + +4. Verify: `cd frontend && npm run build` passes (CSS is valid). + +## Must-Haves + +- [ ] No horizontal overflow at 375px viewport width on creator detail page +- [ ] Stats bar text readable and wrapped at narrow widths +- [ ] Featured technique card contained within viewport +- [ ] Technique grid single-column at 480px and below +- [ ] Edit form (from T01) usable at 375px — full-width inputs, readable labels + +## Verification + +- `cd frontend && npm run build` exits 0 +- `grep -q '480px' frontend/src/App.css` — narrow breakpoint exists + +## Inputs + +- ``frontend/src/App.css` — current responsive breakpoints and creator CSS from S01/S02 and T01` +- ``frontend/src/pages/CreatorDetail.tsx` — class names used in the component (read-only reference)` + +## Expected Output + +- ``frontend/src/App.css` — 480px media query block with creator page mobile fixes` + +## Verification + +cd frontend && npm run build diff --git a/frontend/src/App.css b/frontend/src/App.css index 4d8a3e6..50e22d6 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2721,6 +2721,148 @@ a.app-footer__repo:hover { height: 1.25rem; } +/* Creator Hero — Edit Mode */ +.creator-hero__name-row { + display: flex; + align-items: baseline; + gap: 0.75rem; +} + +.creator-hero__edit-btn { + background: none; + border: 1px solid var(--color-border); + color: var(--color-text-muted); + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; + white-space: nowrap; +} + +.creator-hero__edit-btn:hover { + color: var(--color-accent); + border-color: var(--color-accent); +} + +.creator-edit-form { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-width: 36rem; +} + +.creator-edit-form__label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + margin-bottom: -0.375rem; +} + +.creator-edit-form__bio { + width: 100%; + min-height: 5rem; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-surface); + color: var(--color-text); + font-family: inherit; + font-size: 0.9375rem; + line-height: 1.6; + resize: vertical; +} + +.creator-edit-form__bio:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 2px rgba(var(--color-accent-rgb, 99, 102, 241), 0.15); +} + +.creator-edit-form__links { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.creator-edit-form__link-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.creator-edit-form__platform-select { + flex: 0 0 8rem; + padding: 0.375rem 0.5rem; + border: 1px solid var(--color-border); + border-radius: 4px; + background: var(--color-surface); + color: var(--color-text); + font-size: 0.875rem; +} + +.creator-edit-form__url-input { + flex: 1; + min-width: 0; + padding: 0.375rem 0.5rem; + border: 1px solid var(--color-border); + border-radius: 4px; + background: var(--color-surface); + color: var(--color-text); + font-size: 0.875rem; +} + +.creator-edit-form__url-input:focus, +.creator-edit-form__platform-select:focus { + outline: none; + border-color: var(--color-accent); +} + +.creator-edit-form__remove-btn { + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + font-size: 1rem; + padding: 0.25rem; + line-height: 1; + border-radius: 4px; +} + +.creator-edit-form__remove-btn:hover { + color: var(--color-danger, #ef4444); +} + +.creator-edit-form__add-btn { + align-self: flex-start; + background: none; + border: 1px dashed var(--color-border); + color: var(--color-text-muted); + font-size: 0.8125rem; + padding: 0.375rem 0.75rem; + border-radius: 4px; + cursor: pointer; +} + +.creator-edit-form__add-btn:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + +.creator-edit-form__error { + color: var(--color-danger, #ef4444); + font-size: 0.8125rem; + padding: 0.375rem 0; +} + +.creator-edit-form__actions { + display: flex; + gap: 0.5rem; + margin-top: 0.25rem; +} + .creator-detail__stats-bar { display: flex; align-items: center; diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 723ebc0..de4c659 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -811,6 +811,31 @@ export interface AdminTechniquePageListResponse { limit: number; } +// ── Admin: Creator Profile ────────────────────────────────────────────────── + +export interface UpdateCreatorProfilePayload { + bio?: string | null; + social_links?: Record | null; + featured?: boolean; + avatar_url?: string | null; +} + +export interface UpdateCreatorProfileResponse { + status: string; + creator: string; + fields: string[]; +} + +export async function updateCreatorProfile( + creatorId: string, + payload: UpdateCreatorProfilePayload, +): Promise { + return request( + `${BASE}/admin/pipeline/creators/${creatorId}`, + { method: "PUT", body: JSON.stringify(payload) }, + ); +} + export async function fetchAdminTechniquePages( params: { multi_source_only?: boolean; diff --git a/frontend/src/pages/CreatorDetail.tsx b/frontend/src/pages/CreatorDetail.tsx index 3da76fc..96e6f4a 100644 --- a/frontend/src/pages/CreatorDetail.tsx +++ b/frontend/src/pages/CreatorDetail.tsx @@ -9,6 +9,7 @@ import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { fetchCreator, + updateCreatorProfile, type CreatorDetailResponse, } from "../api/public-client"; import CreatorAvatar from "../components/CreatorAvatar"; @@ -25,6 +26,18 @@ const CREATOR_SORT_OPTIONS = [ { value: "alpha", label: "A–Z" }, ]; +const PLATFORM_OPTIONS = [ + "Instagram", + "YouTube", + "Bandcamp", + "SoundCloud", + "Twitter", + "Spotify", + "Facebook", + "Twitch", + "Website", +]; + export default function CreatorDetail() { const { slug } = useParams<{ slug: string }>(); const [creator, setCreator] = useState(null); @@ -33,6 +46,13 @@ export default function CreatorDetail() { const [error, setError] = useState(null); const [sort, setSort] = useSortPreference("newest"); + // Edit mode state + const [editMode, setEditMode] = useState(false); + const [editBio, setEditBio] = useState(""); + const [editLinks, setEditLinks] = useState>([]); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + useDocumentTitle(creator ? `${creator.name} — Chrysopedia` : "Chrysopedia"); useEffect(() => { @@ -69,6 +89,65 @@ export default function CreatorDetail() { }; }, [slug]); + function enterEditMode() { + if (!creator) return; + setEditBio(creator.bio ?? ""); + setEditLinks( + creator.social_links && Object.keys(creator.social_links).length > 0 + ? Object.entries(creator.social_links).map(([platform, url]) => ({ platform, url })) + : [], + ); + setSaveError(null); + setEditMode(true); + } + + function handleCancel() { + setEditMode(false); + setSaveError(null); + } + + async function handleSave() { + if (!creator) return; + setSaving(true); + setSaveError(null); + + const filteredLinks = editLinks.filter((l) => l.platform && l.url); + const socialLinks: Record | null = + filteredLinks.length > 0 + ? Object.fromEntries(filteredLinks.map((l) => [l.platform.toLowerCase(), l.url])) + : null; + + try { + await updateCreatorProfile(creator.id, { + bio: editBio || null, + social_links: socialLinks, + }); + // Optimistic local update + setCreator({ + ...creator, + bio: editBio || null, + social_links: socialLinks, + }); + setEditMode(false); + } catch (err) { + setSaveError(err instanceof Error ? err.message : "Failed to save"); + } finally { + setSaving(false); + } + } + + function addLinkRow() { + setEditLinks([...editLinks, { platform: PLATFORM_OPTIONS[0] ?? "Instagram", url: "" }]); + } + + function removeLinkRow(index: number) { + setEditLinks(editLinks.filter((_, i) => i !== index)); + } + + function updateLinkRow(index: number, field: "platform" | "url", value: string) { + setEditLinks(editLinks.map((row, i) => (i === index ? { ...row, [field]: value } : row))); + } + if (loading) { return
Loading creator…
; } @@ -120,20 +199,103 @@ export default function CreatorDetail() { size={96} />
-

{creator.name}

- {creator.bio && ( -

{creator.bio}

- )} - {creator.social_links && Object.keys(creator.social_links).length > 0 && ( -
- {Object.entries(creator.social_links).map(([platform, url]) => ( - - - - ))} +
+

{creator.name}

+ {!editMode && ( + + )} +
+ + {editMode ? ( +
+ +