chrysopedia/frontend/src/pages/PostsList.tsx
jlightner 04630764a6 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
2026-04-04 09:17:30 +00:00

191 lines
6.5 KiB
TypeScript

/**
* 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>
);
}