| // src/components/admin/AdminCommentsList.tsx | |
| import { Link } from "react-router-dom"; | |
| import { Trash2, MessageSquare, Clock, RefreshCw } from "lucide-react"; | |
| import { useState } from "react"; | |
| import { toast } from "react-toastify"; | |
| interface AdminComment { | |
| id: number; | |
| articleId: number; | |
| authorUsername: string; | |
| body: string; | |
| createdAt: string; | |
| } | |
| interface AdminCommentsListProps { | |
| comments: AdminComment[]; | |
| onDelete: (id: number) => Promise<void>; | |
| } | |
| export function AdminCommentsList({ comments, onDelete }: AdminCommentsListProps) { | |
| const [loadingStates, setLoadingStates] = useState<Record<number, boolean>>({}); | |
| const handleDelete = async (id: number) => { | |
| setLoadingStates((prev) => ({ ...prev, [id]: true })); | |
| try { | |
| await onDelete(id); | |
| toast.success("Comment deleted successfully"); | |
| } catch (err) { | |
| console.error(err); | |
| } finally { | |
| setLoadingStates((prev) => ({ ...prev, [id]: false })); | |
| } | |
| }; | |
| if (!comments || comments.length === 0) { | |
| return ( | |
| <div className="text-center py-12 bg-white/40 dark:bg-gray-900/40 backdrop-blur-md border border-gray-200 dark:border-gray-800 rounded-2xl text-gray-500"> | |
| No comments have been posted yet. | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-4"> | |
| {comments.map((comment) => { | |
| const isLoading = loadingStates[comment.id] || false; | |
| return ( | |
| <div | |
| key={comment.id} | |
| className="bg-white/40 dark:bg-gray-900/40 backdrop-blur-md border border-gray-200/50 dark:border-gray-800/50 rounded-2xl p-5 shadow-lg flex justify-between items-start gap-4 transition duration-200 hover:shadow-xl hover:border-gray-300 dark:hover:border-gray-700" | |
| > | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm"> | |
| <span className="font-bold text-gray-800 dark:text-gray-200"> | |
| @{comment.authorUsername} | |
| </span> | |
| <span className="text-gray-450 dark:text-gray-400">on</span> | |
| <Link | |
| to={`/articles/${comment.articleId}`} | |
| className="text-primary hover:underline font-semibold text-xs bg-primary/10 dark:bg-primary/20 px-2 py-0.5 rounded-full" | |
| > | |
| article #{comment.articleId} | |
| </Link> | |
| </div> | |
| <p className="text-sm mt-2.5 text-gray-700 dark:text-gray-300 leading-relaxed font-light break-words"> | |
| {comment.body} | |
| </p> | |
| <div className="flex items-center gap-1 text-[11px] text-gray-400 mt-3 font-medium"> | |
| <Clock className="w-3.5 h-3.5" /> | |
| {new Date(comment.createdAt).toLocaleString()} | |
| </div> | |
| </div> | |
| <div className="shrink-0 pt-1"> | |
| {isLoading ? ( | |
| <RefreshCw className="w-4 h-4 text-gray-400 animate-spin" /> | |
| ) : ( | |
| <button | |
| onClick={() => handleDelete(comment.id)} | |
| className="p-2 text-gray-400 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition duration-200" | |
| title="Delete Comment" | |
| > | |
| <Trash2 className="w-4.5 h-4.5" /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 3.48 kB
- Xet hash:
- ee24aa107f26cc1eac0ba5b32e71aab8414a456286484d1e3d56c9634cb3e275
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.