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:
parent
47014f5a3f
commit
18eb4e0ee8
12 changed files with 929 additions and 14 deletions
|
|
@ -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. |
|
||||
|
|
|
|||
93
.gsd/milestones/M017/slices/S03/S03-SUMMARY.md
Normal file
93
.gsd/milestones/M017/slices/S03/S03-SUMMARY.md
Normal 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
|
||||
49
.gsd/milestones/M017/slices/S03/S03-UAT.md
Normal file
49
.gsd/milestones/M017/slices/S03/S03-UAT.md
Normal 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
|
||||
48
.gsd/milestones/M017/slices/S03/tasks/T02-VERIFY.json
Normal file
48
.gsd/milestones/M017/slices/S03/tasks/T02-VERIFY.json
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
54
.gsd/milestones/M017/slices/S04/S04-RESEARCH.md
Normal file
54
.gsd/milestones/M017/slices/S04/S04-RESEARCH.md
Normal 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
|
||||
96
.gsd/milestones/M017/slices/S04/tasks/T01-PLAN.md
Normal file
96
.gsd/milestones/M017/slices/S04/tasks/T01-PLAN.md
Normal 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
|
||||
82
.gsd/milestones/M017/slices/S04/tasks/T01-SUMMARY.md
Normal file
82
.gsd/milestones/M017/slices/S04/tasks/T01-SUMMARY.md
Normal 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.
|
||||
54
.gsd/milestones/M017/slices/S04/tasks/T02-PLAN.md
Normal file
54
.gsd/milestones/M017/slices/S04/tasks/T02-PLAN.md
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<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) => (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue