feat: Added PostsFeed component to creator profile pages with Tiptap HT…
- "frontend/src/components/PostsFeed.tsx" - "frontend/src/components/PostsFeed.module.css" - "frontend/src/pages/PostsList.tsx" - "frontend/src/pages/PostsList.module.css" - "frontend/src/pages/CreatorDetail.tsx" - "frontend/src/App.tsx" - "frontend/src/pages/CreatorDashboard.tsx" GSD-Task: S01/T04
This commit is contained in:
parent
9139d5a93a
commit
9431aa2095
13 changed files with 1084 additions and 4 deletions
|
|
@ -207,7 +207,7 @@ Build the creator-facing post editor with Tiptap rich text editing and file atta
|
|||
- Estimate: 1h30m
|
||||
- Files: frontend/package.json, frontend/src/api/client.ts, frontend/src/api/posts.ts, frontend/src/pages/PostEditor.tsx, frontend/src/pages/PostEditor.module.css, frontend/src/App.tsx, frontend/src/pages/CreatorDashboard.tsx
|
||||
- Verify: cd frontend && npm run build 2>&1 | tail -5
|
||||
- [ ] **T04: Posts feed on creator profile with file download buttons** — ## Description
|
||||
- [x] **T04: Added PostsFeed component to creator profile pages with Tiptap HTML rendering and signed-URL file downloads, plus PostsList management page for creators** — ## Description
|
||||
|
||||
Build the public-facing read path: a PostsFeed component on the creator detail page showing published posts reverse-chronologically, with file download buttons using signed URLs. Also add a posts list page at `/creator/posts` for the creator to manage their posts.
|
||||
|
||||
|
|
|
|||
16
.gsd/milestones/M023/slices/S01/tasks/T03-VERIFY.json
Normal file
16
.gsd/milestones/M023/slices/S01/tasks/T03-VERIFY.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T03",
|
||||
"unitId": "M023/S01/T03",
|
||||
"timestamp": 1775294028849,
|
||||
"passed": true,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "cd frontend",
|
||||
"exitCode": 0,
|
||||
"durationMs": 8,
|
||||
"verdict": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
87
.gsd/milestones/M023/slices/S01/tasks/T04-SUMMARY.md
Normal file
87
.gsd/milestones/M023/slices/S01/tasks/T04-SUMMARY.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
id: T04
|
||||
parent: S01
|
||||
milestone: M023
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["frontend/src/components/PostsFeed.tsx", "frontend/src/components/PostsFeed.module.css", "frontend/src/pages/PostsList.tsx", "frontend/src/pages/PostsList.module.css", "frontend/src/pages/CreatorDetail.tsx", "frontend/src/App.tsx", "frontend/src/pages/CreatorDashboard.tsx"]
|
||||
key_decisions: ["PostsFeed returns null when total === 0 — hides section on public profile", "SidebarNav changed from New Post to Posts management link"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Frontend build succeeds — PostsFeed and PostsList compile and chunk correctly. All route definitions valid."
|
||||
completed_at: 2026-04-04T09:17:22.826Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T04: Added PostsFeed component to creator profile pages with Tiptap HTML rendering and signed-URL file downloads, plus PostsList management page for creators
|
||||
|
||||
> Added PostsFeed component to creator profile pages with Tiptap HTML rendering and signed-URL file downloads, plus PostsList management page for creators
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T04
|
||||
parent: S01
|
||||
milestone: M023
|
||||
key_files:
|
||||
- frontend/src/components/PostsFeed.tsx
|
||||
- frontend/src/components/PostsFeed.module.css
|
||||
- frontend/src/pages/PostsList.tsx
|
||||
- frontend/src/pages/PostsList.module.css
|
||||
- frontend/src/pages/CreatorDetail.tsx
|
||||
- frontend/src/App.tsx
|
||||
- frontend/src/pages/CreatorDashboard.tsx
|
||||
key_decisions:
|
||||
- PostsFeed returns null when total === 0 — hides section on public profile
|
||||
- SidebarNav changed from New Post to Posts management link
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-04T09:17:22.827Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T04: Added PostsFeed component to creator profile pages with Tiptap HTML rendering and signed-URL file downloads, plus PostsList management page for creators
|
||||
|
||||
**Added PostsFeed component to creator profile pages with Tiptap HTML rendering and signed-URL file downloads, plus PostsList management page for creators**
|
||||
|
||||
## What Happened
|
||||
|
||||
Created PostsFeed component that renders published posts with Tiptap JSON→HTML conversion and file attachment download buttons using signed URLs. Integrated into CreatorDetail page. Created PostsList management page at /creator/posts with table/card layouts, status badges, edit/delete actions with confirmation dialog. Updated SidebarNav and App.tsx routes.
|
||||
|
||||
## Verification
|
||||
|
||||
Frontend build succeeds — PostsFeed and PostsList compile and chunk correctly. All route definitions valid.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `cd frontend && npm run build 2>&1 | tail -5` | 0 | ✅ pass | 6700ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
Changed SidebarNav 'New Post' direct link to 'Posts' management page link — more useful with the management page now available.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/components/PostsFeed.tsx`
|
||||
- `frontend/src/components/PostsFeed.module.css`
|
||||
- `frontend/src/pages/PostsList.tsx`
|
||||
- `frontend/src/pages/PostsList.module.css`
|
||||
- `frontend/src/pages/CreatorDetail.tsx`
|
||||
- `frontend/src/App.tsx`
|
||||
- `frontend/src/pages/CreatorDashboard.tsx`
|
||||
|
||||
|
||||
## Deviations
|
||||
Changed SidebarNav 'New Post' direct link to 'Posts' management page link — more useful with the management page now available.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
113
frontend/package-lock.json
generated
113
frontend/package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@tiptap/extension-link": "^3.22.1",
|
||||
"@tiptap/extension-placeholder": "^3.22.1",
|
||||
"@tiptap/html": "^3.22.1",
|
||||
"@tiptap/pm": "^3.22.1",
|
||||
"@tiptap/react": "^3.22.1",
|
||||
"@tiptap/starter-kit": "^3.22.1",
|
||||
|
|
@ -1555,6 +1556,21 @@
|
|||
"@tiptap/pm": "^3.22.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/html": {
|
||||
"version": "3.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-3.22.1.tgz",
|
||||
"integrity": "sha512-EC+BSHDRnHOWlu8aqImE8PtPO6S/swHQYFA+dLpmgyS6fVtSz/VF7jZLRPjVvAX5KXKeXfBvveIbQEAYqhwVnA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.22.1",
|
||||
"@tiptap/pm": "^3.22.1",
|
||||
"happy-dom": "^20.8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.1.tgz",
|
||||
|
|
@ -1722,6 +1738,16 @@
|
|||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
|
||||
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
|
|
@ -1753,6 +1779,23 @@
|
|||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-mimetype": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
|
|
@ -2020,6 +2063,37 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/happy-dom": {
|
||||
"version": "20.8.9",
|
||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.9.tgz",
|
||||
"integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": ">=20.0.0",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"entities": "^7.0.1",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/happy-dom/node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.15",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
|
||||
|
|
@ -2594,6 +2668,13 @@
|
|||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
|
|
@ -2721,6 +2802,38 @@
|
|||
"integrity": "sha512-MSZcA13R9ZlxgYpzfakaSYf8dz5tCdZKYbjtN1qnKbCi+UoyfaTuhvjlXHrITi/fgeO3qWfsH7U3BP1AKnwRNg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@tiptap/extension-link": "^3.22.1",
|
||||
"@tiptap/extension-placeholder": "^3.22.1",
|
||||
"@tiptap/html": "^3.22.1",
|
||||
"@tiptap/pm": "^3.22.1",
|
||||
"@tiptap/react": "^3.22.1",
|
||||
"@tiptap/starter-kit": "^3.22.1",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const ChapterReview = React.lazy(() => import("./pages/ChapterReview"));
|
|||
const HighlightQueue = React.lazy(() => import("./pages/HighlightQueue"));
|
||||
const CreatorTiers = React.lazy(() => import("./pages/CreatorTiers"));
|
||||
const PostEditor = React.lazy(() => import("./pages/PostEditor"));
|
||||
const PostsList = React.lazy(() => import("./pages/PostsList"));
|
||||
import AdminDropdown from "./components/AdminDropdown";
|
||||
import ImpersonationBanner from "./components/ImpersonationBanner";
|
||||
import AppFooter from "./components/AppFooter";
|
||||
|
|
@ -208,6 +209,7 @@ function AppShell() {
|
|||
<Route path="/creator/chapters/:videoId" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ChapterReview /></Suspense></ProtectedRoute>} />
|
||||
<Route path="/creator/highlights" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><HighlightQueue /></Suspense></ProtectedRoute>} />
|
||||
<Route path="/creator/tiers" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><CreatorTiers /></Suspense></ProtectedRoute>} />
|
||||
<Route path="/creator/posts" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostsList /></Suspense></ProtectedRoute>} />
|
||||
<Route path="/creator/posts/new" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />
|
||||
<Route path="/creator/posts/:postId/edit" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />
|
||||
|
||||
|
|
|
|||
201
frontend/src/components/PostsFeed.module.css
Normal file
201
frontend/src/components/PostsFeed.module.css
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/* PostsFeed.module.css — dark theme post cards for creator profile */
|
||||
|
||||
.section {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.postCount {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Feed list ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* ── Post card ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.card {
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--color-border-active, var(--color-border));
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.cardMeta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted, var(--color-text-secondary));
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ── Rich text body ────────────────────────────────────────────────────────── */
|
||||
|
||||
.cardBody {
|
||||
font-size: 0.925rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cardBody h1,
|
||||
.cardBody h2,
|
||||
.cardBody h3 {
|
||||
color: var(--color-text-primary);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cardBody p {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.cardBody ul,
|
||||
.cardBody ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.cardBody blockquote {
|
||||
border-left: 3px solid var(--color-accent, #22d3ee);
|
||||
padding-left: 1rem;
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--color-text-muted, var(--color-text-secondary));
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cardBody a {
|
||||
color: var(--color-accent, #22d3ee);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cardBody code {
|
||||
background: var(--color-bg-input, #1e293b);
|
||||
padding: 0.15em 0.35em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.cardBody pre {
|
||||
background: var(--color-bg-input, #1e293b);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Attachments ───────────────────────────────────────────────────────────── */
|
||||
|
||||
.attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-bg-input, #1e293b);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.825rem;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.attachment:hover {
|
||||
background: var(--color-bg-surface-hover, #334155);
|
||||
border-color: var(--color-accent, #22d3ee);
|
||||
}
|
||||
|
||||
.attachmentIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-accent, #22d3ee);
|
||||
}
|
||||
|
||||
.attachmentName {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attachmentSize {
|
||||
color: var(--color-text-muted, var(--color-text-secondary));
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── States ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.loading {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--color-text-muted, var(--color-text-secondary));
|
||||
font-size: 0.925rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-error, #ef4444);
|
||||
font-size: 0.925rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* ── Responsive ────────────────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
165
frontend/src/components/PostsFeed.tsx
Normal file
165
frontend/src/components/PostsFeed.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* PostsFeed — Renders a reverse-chronological feed of published posts
|
||||
* for a given creator. Used on the public creator profile page.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { generateHTML } from "@tiptap/html";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import {
|
||||
listPosts,
|
||||
getDownloadUrl,
|
||||
type PostRead,
|
||||
} from "../api/posts";
|
||||
import styles from "./PostsFeed.module.css";
|
||||
|
||||
// Tiptap extensions matching those in PostEditor — needed for generateHTML
|
||||
const extensions = [StarterKit, Link];
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function FileIcon() {
|
||||
return (
|
||||
<svg className={styles.attachmentIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentButton({ attachment }: { attachment: PostRead["attachments"][number] }) {
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
if (downloading) return;
|
||||
setDownloading(true);
|
||||
try {
|
||||
const { url } = await getDownloadUrl(String(attachment.id));
|
||||
window.open(url, "_blank", "noopener");
|
||||
} catch (err) {
|
||||
console.error("Download failed:", err);
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
}, [attachment.id, downloading]);
|
||||
|
||||
return (
|
||||
<button className={styles.attachment} onClick={handleDownload} disabled={downloading}>
|
||||
<FileIcon />
|
||||
<span className={styles.attachmentName}>{attachment.filename}</span>
|
||||
<span className={styles.attachmentSize}>{formatFileSize(attachment.size_bytes)}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBody(bodyJson: Record<string, unknown>): string {
|
||||
try {
|
||||
return generateHTML(bodyJson as Parameters<typeof generateHTML>[0], extensions);
|
||||
} catch {
|
||||
// Fallback: if body_json isn't valid Tiptap JSON, try plain text
|
||||
return "<p>(Content unavailable)</p>";
|
||||
}
|
||||
}
|
||||
|
||||
interface PostsFeedProps {
|
||||
creatorId: number | string;
|
||||
}
|
||||
|
||||
export default function PostsFeed({ creatorId }: PostsFeedProps) {
|
||||
const [posts, setPosts] = useState<PostRead[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
listPosts(String(creatorId), 1, 50)
|
||||
.then((res) => {
|
||||
if (!cancelled) {
|
||||
setPosts(res.items);
|
||||
setTotal(res.total);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load posts");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [creatorId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2 className={styles.sectionTitle}>Posts</h2>
|
||||
</div>
|
||||
<div className={styles.loading}>Loading posts…</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2 className={styles.sectionTitle}>Posts</h2>
|
||||
</div>
|
||||
<div className={styles.error}>{error}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
return null; // Don't show empty posts section on public profile
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2 className={styles.sectionTitle}>Posts</h2>
|
||||
<span className={styles.postCount}>({total})</span>
|
||||
</div>
|
||||
<div className={styles.feed}>
|
||||
{posts.map((post) => (
|
||||
<article key={post.id} className={styles.card}>
|
||||
<h3 className={styles.cardTitle}>{post.title}</h3>
|
||||
<div className={styles.cardMeta}>{formatDate(post.created_at)}</div>
|
||||
<div
|
||||
className={styles.cardBody}
|
||||
dangerouslySetInnerHTML={{ __html: renderBody(post.body_json) }}
|
||||
/>
|
||||
{post.attachments.length > 0 && (
|
||||
<div className={styles.attachments}>
|
||||
{post.attachments.map((att) => (
|
||||
<AttachmentButton key={att.id} attachment={att} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -59,12 +59,12 @@ function SidebarNav() {
|
|||
</svg>
|
||||
Tiers
|
||||
</NavLink>
|
||||
<NavLink to="/creator/posts/new" className={linkClass}>
|
||||
<NavLink to="/creator/posts" className={linkClass}>
|
||||
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
New Post
|
||||
Posts
|
||||
</NavLink>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useAuth } from "../context/AuthContext";
|
|||
import CreatorAvatar from "../components/CreatorAvatar";
|
||||
import { SocialIcon } from "../components/SocialIcons";
|
||||
import ChatWidget from "../components/ChatWidget";
|
||||
import PostsFeed from "../components/PostsFeed";
|
||||
import PersonalityProfile from "../components/PersonalityProfile";
|
||||
import SortDropdown from "../components/SortDropdown";
|
||||
import TagList from "../components/TagList";
|
||||
|
|
@ -484,6 +485,9 @@ export default function CreatorDetail() {
|
|||
)}
|
||||
</section>
|
||||
|
||||
{/* Posts feed */}
|
||||
<PostsFeed creatorId={creator.id} />
|
||||
|
||||
<ChatWidget creatorName={creator.name} techniques={creator.techniques} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
300
frontend/src/pages/PostsList.module.css
Normal file
300
frontend/src/pages/PostsList.module.css
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
/* PostsList.module.css — creator post management list */
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2rem 2.5rem;
|
||||
}
|
||||
|
||||
/* ── Header ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pageTitle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.newBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--color-accent, #22d3ee);
|
||||
color: var(--color-bg, #0f172a);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.newBtn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ── Table (desktop) ───────────────────────────────────────────────────────── */
|
||||
|
||||
.tableWrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-muted, var(--color-text-secondary));
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tr:hover td {
|
||||
background: var(--color-bg-surface-hover, rgba(255, 255, 255, 0.03));
|
||||
}
|
||||
|
||||
.titleCell {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* ── Status badges ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.725rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.badgePublished {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.badgeDraft {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ── Actions ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.editBtn,
|
||||
.deleteBtn {
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editBtn {
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.editBtn:hover {
|
||||
background: var(--color-bg-surface-hover, #334155);
|
||||
border-color: var(--color-accent, #22d3ee);
|
||||
color: var(--color-accent, #22d3ee);
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.deleteBtn:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.deleteBtn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Mobile cards ──────────────────────────────────────────────────────────── */
|
||||
|
||||
.mobileCards {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── States ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.loading {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-error, #ef4444);
|
||||
font-size: 0.95rem;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--color-text-muted, var(--color-text-secondary));
|
||||
font-size: 0.925rem;
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Confirm dialog ────────────────────────────────────────────────────────── */
|
||||
|
||||
.confirmOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.confirmDialog {
|
||||
background: var(--color-bg-surface, #1e293b);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.confirmTitle {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.confirmText {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.confirmActions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.confirmCancel {
|
||||
padding: 0.45rem 1rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.confirmDelete {
|
||||
padding: 0.45rem 1rem;
|
||||
background: #ef4444;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.confirmDelete:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Responsive ────────────────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.tableWrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileCards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.mobileCard {
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mobileCardTitle {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.925rem;
|
||||
}
|
||||
|
||||
.mobileCardMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mobileCardActions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
191
frontend/src/pages/PostsList.tsx
Normal file
191
frontend/src/pages/PostsList.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/**
|
||||
* PostsList — Creator post management page.
|
||||
*
|
||||
* Shows all posts (draft + published) with edit, delete, and status controls.
|
||||
* Protected route at /creator/posts.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
||||
import { listPosts, deletePost, type PostRead } from "../api/posts";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { SidebarNav } from "./CreatorDashboard";
|
||||
import styles from "./PostsList.module.css";
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export default function PostsList() {
|
||||
useDocumentTitle("My Posts — Chrysopedia");
|
||||
const { user } = useAuth();
|
||||
|
||||
const [posts, setPosts] = useState<PostRead[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Delete confirmation
|
||||
const [confirmDelete, setConfirmDelete] = useState<PostRead | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.creator_id) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
listPosts(String(user.creator_id), 1, 100)
|
||||
.then((res) => {
|
||||
if (!cancelled) setPosts(res.items);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load posts");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [user?.creator_id]);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirmDelete) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deletePost(String(confirmDelete.id));
|
||||
setPosts((prev) => prev.filter((p) => p.id !== confirmDelete.id));
|
||||
setConfirmDelete(null);
|
||||
} catch (err) {
|
||||
console.error("Delete failed:", err);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<SidebarNav />
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.pageTitle}>My Posts</h1>
|
||||
<Link to="/creator/posts/new" className={styles.newBtn}>
|
||||
+ New Post
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{loading && <div className={styles.loading}>Loading posts…</div>}
|
||||
|
||||
{!loading && error && <div className={styles.error}>{error}</div>}
|
||||
|
||||
{!loading && !error && posts.length === 0 && (
|
||||
<div className={styles.empty}>
|
||||
<p>No posts yet. Create your first post to share with followers.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && posts.length > 0 && (
|
||||
<>
|
||||
{/* Desktop table */}
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{posts.map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td className={styles.titleCell}>{post.title}</td>
|
||||
<td>
|
||||
<span className={`${styles.badge} ${post.is_published ? styles.badgePublished : styles.badgeDraft}`}>
|
||||
{post.is_published ? "Published" : "Draft"}
|
||||
</span>
|
||||
</td>
|
||||
<td>{formatDate(post.created_at)}</td>
|
||||
<td>
|
||||
<div className={styles.actions}>
|
||||
<Link to={`/creator/posts/${post.id}/edit`} className={styles.editBtn}>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
className={styles.deleteBtn}
|
||||
onClick={() => setConfirmDelete(post)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile cards */}
|
||||
<div className={styles.mobileCards}>
|
||||
{posts.map((post) => (
|
||||
<div key={post.id} className={styles.mobileCard}>
|
||||
<span className={styles.mobileCardTitle}>{post.title}</span>
|
||||
<div className={styles.mobileCardMeta}>
|
||||
<span className={`${styles.badge} ${post.is_published ? styles.badgePublished : styles.badgeDraft}`}>
|
||||
{post.is_published ? "Published" : "Draft"}
|
||||
</span>
|
||||
<span>{formatDate(post.created_at)}</span>
|
||||
</div>
|
||||
<div className={styles.mobileCardActions}>
|
||||
<Link to={`/creator/posts/${post.id}/edit`} className={styles.editBtn}>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
className={styles.deleteBtn}
|
||||
onClick={() => setConfirmDelete(post)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
{confirmDelete && (
|
||||
<div className={styles.confirmOverlay} onClick={() => !deleting && setConfirmDelete(null)}>
|
||||
<div className={styles.confirmDialog} onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className={styles.confirmTitle}>Delete Post</h3>
|
||||
<p className={styles.confirmText}>
|
||||
Are you sure you want to delete "{confirmDelete.title}"? This cannot be undone.
|
||||
</p>
|
||||
<div className={styles.confirmActions}>
|
||||
<button
|
||||
className={styles.confirmCancel}
|
||||
onClick={() => setConfirmDelete(null)}
|
||||
disabled={deleting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className={styles.confirmDelete}
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? "Deleting…" : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"}
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/PostsFeed.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/PostsList.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"}
|
||||
Loading…
Add table
Reference in a new issue