feat: Added updateCreatorProfile() API client and inline bio/social-lin…

- "frontend/src/api/public-client.ts"
- "frontend/src/pages/CreatorDetail.tsx"
- "frontend/src/App.css"

GSD-Task: S04/T01
This commit is contained in:
jlightner 2026-04-03 09:18:39 +00:00
parent 47014f5a3f
commit 18eb4e0ee8
12 changed files with 929 additions and 14 deletions

View file

@ -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. |

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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<string, string> | 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<UpdateCreatorProfileResponse> {
return request<UpdateCreatorProfileResponse>(
`${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<Array<{platform: string; url: string}>>([])`
- `const [saving, setSaving] = useState(false)`
- `const [saveError, setSaveError] = useState<string | null>(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 `<select>` for platform (Instagram, YouTube, Bandcamp, SoundCloud, Twitter, Spotify, Facebook, Twitch, Website) + `<input type="url">` 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

View file

@ -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<T>(\`${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<string, string>` (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

View file

@ -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<string, string> | 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<UpdateCreatorProfileResponse> {
return request<UpdateCreatorProfileResponse>(
`${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<Array<{platform: string; url: string}>>([])`
- `const [saving, setSaving] = useState(false)`
- `const [saveError, setSaveError] = useState<string | null>(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 `<select>` for platform (Instagram, YouTube, Bandcamp, SoundCloud, Twitter, Spotify, Facebook, Twitch, Website) + `<input type="url">` 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<T> 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

View file

@ -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.

View file

@ -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

View file

@ -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;

View file

@ -811,6 +811,31 @@ export interface AdminTechniquePageListResponse {
limit: number;
}
// ── Admin: Creator Profile ──────────────────────────────────────────────────
export interface UpdateCreatorProfilePayload {
bio?: string | null;
social_links?: Record<string, string> | 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<UpdateCreatorProfileResponse> {
return request<UpdateCreatorProfileResponse>(
`${BASE}/admin/pipeline/creators/${creatorId}`,
{ method: "PUT", body: JSON.stringify(payload) },
);
}
export async function fetchAdminTechniquePages(
params: {
multi_source_only?: boolean;

View file

@ -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: "AZ" },
];
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<CreatorDetailResponse | null>(null);
@ -33,6 +46,13 @@ export default function CreatorDetail() {
const [error, setError] = useState<string | null>(null);
const [sort, setSort] = useSortPreference("newest");
// Edit mode state
const [editMode, setEditMode] = useState(false);
const [editBio, setEditBio] = useState("");
const [editLinks, setEditLinks] = useState<Array<{ platform: string; url: string }>>([]);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(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<string, string> | 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 <div className="loading">Loading creator</div>;
}
@ -120,20 +199,103 @@ export default function CreatorDetail() {
size={96}
/>
<div className="creator-hero__info">
<h1 className="creator-hero__name">{creator.name}</h1>
{creator.bio && (
<p className="creator-hero__bio">{creator.bio}</p>
)}
{creator.social_links && Object.keys(creator.social_links).length > 0 && (
<div className="creator-hero__socials">
{Object.entries(creator.social_links).map(([platform, url]) => (
<a key={platform} href={url} target="_blank" rel="noopener noreferrer"
className="creator-hero__social-link" title={platform}>
<SocialIcon platform={platform} />
</a>
))}
<div className="creator-hero__name-row">
<h1 className="creator-hero__name">{creator.name}</h1>
{!editMode && (
<button
className="creator-hero__edit-btn"
onClick={enterEditMode}
title="Edit profile"
>
Edit
</button>
)}
</div>
{editMode ? (
<div className="creator-edit-form">
<label className="creator-edit-form__label">Bio</label>
<textarea
className="creator-edit-form__bio"
value={editBio}
onChange={(e) => setEditBio(e.target.value)}
rows={4}
placeholder="Creator bio…"
/>
<label className="creator-edit-form__label">Social Links</label>
<div className="creator-edit-form__links">
{editLinks.map((link, i) => (
<div key={i} className="creator-edit-form__link-row">
<select
value={link.platform}
onChange={(e) => updateLinkRow(i, "platform", e.target.value)}
className="creator-edit-form__platform-select"
>
{PLATFORM_OPTIONS.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</select>
<input
type="url"
className="creator-edit-form__url-input"
value={link.url}
onChange={(e) => updateLinkRow(i, "url", e.target.value)}
placeholder="https://…"
/>
<button
className="creator-edit-form__remove-btn"
onClick={() => removeLinkRow(i)}
title="Remove link"
>
</button>
</div>
))}
<button className="creator-edit-form__add-btn" onClick={addLinkRow}>
+ Add Link
</button>
</div>
{saveError && (
<div className="creator-edit-form__error">{saveError}</div>
)}
<div className="creator-edit-form__actions">
<button
className="btn btn--primary"
onClick={handleSave}
disabled={saving}
>
{saving ? "Saving…" : "Save"}
</button>
<button
className="btn btn--secondary"
onClick={handleCancel}
disabled={saving}
>
Cancel
</button>
</div>
</div>
) : (
<>
{creator.bio && (
<p className="creator-hero__bio">{creator.bio}</p>
)}
{creator.social_links && Object.keys(creator.social_links).length > 0 && (
<div className="creator-hero__socials">
{Object.entries(creator.social_links).map(([platform, url]) => (
<a key={platform} href={url} target="_blank" rel="noopener noreferrer"
className="creator-hero__social-link" title={platform}>
<SocialIcon platform={platform} />
</a>
))}
</div>
)}
</>
)}
{creator.genres && creator.genres.length > 0 && (
<div className="creator-hero__genres">
{creator.genres.map((g) => (