tahamajs/IE / Projects /front /src /components /admin /ArticleTable.tsx
tahamajs's picture
download
raw
4.13 kB
import { useEffect, useMemo, useState } from "react";
import api from "@/lib/api";
import { Trash2, Search, Loader2 } from "lucide-react";
import { toast } from "react-toastify";
interface Article {
id: number;
title: string;
author?: { username?: string };
status?: string;
createdAt?: string;
}
export function ArticleTable() {
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
useEffect(() => {
api
.get("/admin/articles")
.then((res) => setArticles(Array.isArray(res.data) ? res.data : res.data?.articles ?? []))
.catch(() => toast.error("Failed to load articles"))
.finally(() => setLoading(false));
}, []);
const filtered = useMemo(
() =>
articles.filter((article) =>
`${article.title} ${article.author?.username ?? ""}`.toLowerCase().includes(search.toLowerCase())
),
[articles, search]
);
const deleteArticle = async (id: number) => {
if (!confirm("Delete this article? This action cannot be undone.")) return;
try {
await api.delete(`/admin/articles/${id}`);
setArticles((prev) => prev.filter((article) => article.id !== id));
toast.success("Article deleted");
} catch {
toast.error("Failed to delete article");
}
};
return (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden">
<div className="p-4 border-b">
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search articles..."
className="w-full pl-9 pr-3 py-2 border rounded-lg dark:bg-gray-700 text-sm"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-gray-700/50 border-b">
<tr>
<th className="px-4 py-3 text-left font-semibold">ID</th>
<th className="px-4 py-3 text-left font-semibold">Title</th>
<th className="px-4 py-3 text-left font-semibold">Author</th>
<th className="px-4 py-3 text-left font-semibold">Status</th>
<th className="px-4 py-3 text-left font-semibold">Created</th>
<th className="px-4 py-3 text-left font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={6} className="text-center py-8">
<Loader2 className="w-5 h-5 mx-auto animate-spin" />
</td>
</tr>
) : filtered.length === 0 ? (
<tr>
<td colSpan={6} className="text-center py-8 text-gray-400">
No articles found
</td>
</tr>
) : (
filtered.map((article) => (
<tr key={article.id} className="border-b last:border-0">
<td className="px-4 py-3">{article.id}</td>
<td className="px-4 py-3">{article.title}</td>
<td className="px-4 py-3">{article.author?.username ?? "-"}</td>
<td className="px-4 py-3">{article.status ?? "-"}</td>
<td className="px-4 py-3">
{article.createdAt ? new Date(article.createdAt).toLocaleDateString() : "-"}
</td>
<td className="px-4 py-3">
<button
onClick={() => deleteArticle(article.id)}
className="inline-flex items-center gap-1 text-red-500 hover:text-red-600"
>
<Trash2 className="w-4 h-4" />
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}

Xet Storage Details

Size:
4.13 kB
·
Xet hash:
b43e1a111a2a76e8670ba294850c91ca1005311a28cb1dbf4f40109c1468a090

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.