admin / Bid-Sentinel
publicBid Scrape and Tracking Application with AI Capability
Bid-Sentinel / bid-sentinel-v2 / frontend / src / components / CommentsSection.jsx
3003 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | import { useEffect, useState } from "react"; import { addComment, deleteComment, fetchComments } from "../api/client"; import { useAuth } from "../context/AuthContext.jsx"; export default function CommentsSection({ tenderId }) { const { user } = useAuth(); const [comments, setComments] = useState([]); const [body, setBody] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); useEffect(() => { fetchComments(tenderId) .then(setComments) .catch(() => {}) .finally(() => setLoading(false)); }, [tenderId]); async function submit(e) { e.preventDefault(); const text = body.trim(); if (!text) return; setSaving(true); try { const created = await addComment(tenderId, text); setComments((prev) => [...prev, created]); setBody(""); } finally { setSaving(false); } } async function remove(c) { await deleteComment(tenderId, c.id); setComments((prev) => prev.filter((x) => x.id !== c.id)); } const fmt = (d) => new Date(d).toLocaleString("en-GB"); const canDelete = (c) => c.author_email === user?.email || user?.role === "admin"; return ( <div> <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400"> Comments </p> <div className="mb-3 space-y-2"> {loading && <p className="text-xs text-slate-400">Loading…</p>} {!loading && comments.length === 0 && ( <p className="text-xs text-slate-400">No comments yet.</p> )} {comments.map((c) => ( <div key={c.id} className="rounded-md border border-slate-200 bg-white px-3 py-2"> <div className="mb-0.5 flex items-center justify-between"> <span className="text-xs font-medium text-slate-700">{c.author_email}</span> <span className="flex items-center gap-2 text-xs text-slate-400"> {fmt(c.created_at)} {canDelete(c) && ( <button onClick={() => remove(c)} className="text-slate-400 hover:text-red-500" title="Delete"> ✕ </button> )} </span> </div> <p className="whitespace-pre-wrap text-sm text-slate-700">{c.body}</p> </div> ))} </div> <form onSubmit={submit} className="flex items-start gap-2"> <textarea value={body} onChange={(e) => setBody(e.target.value)} rows={2} placeholder="Add a comment…" className="flex-1 rounded-lg border border-slate-300 px-3 py-2 text-sm outline-none focus:border-brand focus:ring-2 focus:ring-brand/20" /> <button disabled={saving} className="rounded-lg bg-brand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-dark disabled:opacity-60" > {saving ? "…" : "Post"} </button> </form> </div> ); } |