- "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
191 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|