| 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.