Spaces:
Sleeping
Sleeping
Download frontend/src/components/HistoryTable.jsx from haisyam/PlatDetect: direct link, hf CLI and curl.
- Browser
- Download file 7.71 kB
-
https://huggingface.co/spaces/haisyam/PlatDetect/resolve/main/frontend/src/components/HistoryTable.jsx
- Command line
-
hf download hf://spaces/haisyam/PlatDetect/frontend/src/components/HistoryTable.jsx
-
curl -L -o HistoryTable.jsx https://huggingface.co/spaces/haisyam/PlatDetect/resolve/main/frontend/src/components/HistoryTable.jsx
7.71 kB
| import { useEffect, useState } from "react"; | |
| import { History, RefreshCw, CheckCircle, AlertTriangle, Trash2, Trash } from "lucide-react"; | |
| import { getHistory, deleteHistoryRecord, clearAllHistory } from "../services/api"; | |
| import { formatConfidence, formatDateTime } from "../utils/formatters"; | |
| export default function HistoryTable() { | |
| const [history, setHistory] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(""); | |
| const [deletingIndex, setDeletingIndex] = useState(null); | |
| const [confirmClear, setConfirmClear] = useState(false); | |
| async function fetchHistory() { | |
| setLoading(true); | |
| setError(""); | |
| try { | |
| const res = await getHistory(); | |
| if (res.success) { | |
| setHistory(res.data || []); | |
| } else { | |
| setError(res.message || "Gagal memuat riwayat"); | |
| } | |
| } catch { | |
| setError("Riwayat belum tersedia."); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| useEffect(() => { | |
| fetchHistory(); | |
| }, []); | |
| async function handleDelete(index) { | |
| try { | |
| const res = await deleteHistoryRecord(index); | |
| if (res.success) { | |
| setDeletingIndex(null); | |
| fetchHistory(); | |
| } else { | |
| setError(res.message); | |
| } | |
| } catch { | |
| setError("Gagal menghapus riwayat"); | |
| } | |
| } | |
| async function handleClearAll() { | |
| try { | |
| const res = await clearAllHistory(); | |
| if (res.success) { | |
| setConfirmClear(false); | |
| fetchHistory(); | |
| } else { | |
| setError(res.message); | |
| } | |
| } catch { | |
| setError("Gagal menghapus semua riwayat"); | |
| } | |
| } | |
| return ( | |
| <div className="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden"> | |
| <div className="p-4 border-b border-slate-100 flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <History className="w-4 h-4 text-blue-600" /> | |
| <h3 className="font-semibold text-slate-800 text-sm">Riwayat Deteksi</h3> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {history.length > 0 && ( | |
| confirmClear ? ( | |
| <div className="flex items-center gap-1"> | |
| <button | |
| onClick={handleClearAll} | |
| className="text-xs bg-red-600 text-white px-2 py-1 rounded cursor-pointer hover:bg-red-700" | |
| > | |
| Hapus Semua | |
| </button> | |
| <button | |
| onClick={() => setConfirmClear(false)} | |
| className="text-xs text-slate-500 px-2 py-1 cursor-pointer hover:text-slate-700" | |
| > | |
| Batal | |
| </button> | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setConfirmClear(true)} | |
| className="text-slate-400 hover:text-red-500 transition-colors cursor-pointer" | |
| title="Hapus semua riwayat" | |
| > | |
| <Trash className="w-4 h-4" /> | |
| </button> | |
| ) | |
| )} | |
| <button | |
| onClick={fetchHistory} | |
| disabled={loading} | |
| className="text-slate-400 hover:text-blue-600 transition-colors cursor-pointer disabled:opacity-50" | |
| > | |
| <RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} /> | |
| </button> | |
| </div> | |
| </div> | |
| {error && ( | |
| <div className="px-4 py-3 text-sm text-yellow-700 bg-yellow-50"> | |
| {error} | |
| </div> | |
| )} | |
| {loading ? ( | |
| <div className="px-4 py-8 text-center text-sm text-slate-400"> | |
| Memuat riwayat... | |
| </div> | |
| ) : history.length === 0 ? ( | |
| <div className="px-4 py-8 text-center text-sm text-slate-400"> | |
| Belum ada riwayat deteksi. | |
| </div> | |
| ) : ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="bg-slate-50 text-slate-500"> | |
| <th className="px-4 py-2.5 text-left font-medium whitespace-nowrap">Waktu</th> | |
| <th className="px-4 py-2.5 text-left font-medium">File</th> | |
| <th className="px-4 py-2.5 text-left font-medium">Plat</th> | |
| <th className="px-4 py-2.5 text-left font-medium">Status</th> | |
| <th className="px-4 py-2.5 text-left font-medium">Pemilik</th> | |
| <th className="px-4 py-2.5 text-left font-medium">Confidence</th> | |
| <th className="px-4 py-2.5 text-center font-medium">Aksi</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-slate-100"> | |
| {history.map((h, i) => ( | |
| <tr key={i} className="hover:bg-slate-50 transition-colors"> | |
| <td className="px-4 py-2.5 text-slate-500 whitespace-nowrap text-xs"> | |
| {formatDateTime(h.waktu || h.time)} | |
| </td> | |
| <td className="px-4 py-2.5 text-slate-600 truncate max-w-[120px]"> | |
| {h.nama_file || h.file_name || "-"} | |
| </td> | |
| <td className="px-4 py-2.5 font-mono font-medium text-slate-800"> | |
| {h.plate || "-"} | |
| </td> | |
| <td className="px-4 py-2.5"> | |
| {h.status === "Terdaftar" ? ( | |
| <span className="inline-flex items-center gap-1 text-green-700 text-xs bg-green-100 px-2 py-0.5 rounded-full"> | |
| <CheckCircle className="w-3 h-3" /> | |
| Terdaftar | |
| </span> | |
| ) : ( | |
| <span className="inline-flex items-center gap-1 text-yellow-700 text-xs bg-yellow-100 px-2 py-0.5 rounded-full"> | |
| <AlertTriangle className="w-3 h-3" /> | |
| Tidak Terdaftar | |
| </span> | |
| )} | |
| </td> | |
| <td className="px-4 py-2.5 text-slate-700"> | |
| {h.owner_name || h.owner || "-"} | |
| </td> | |
| <td className="px-4 py-2.5 text-slate-600"> | |
| {formatConfidence(h.confidence_yolo)} | |
| </td> | |
| <td className="px-4 py-2.5"> | |
| <div className="flex justify-center"> | |
| {deletingIndex === i ? ( | |
| <div className="flex items-center gap-1"> | |
| <button | |
| onClick={() => handleDelete(i)} | |
| className="text-xs bg-red-600 text-white px-2 py-1 rounded cursor-pointer hover:bg-red-700" | |
| > | |
| Hapus | |
| </button> | |
| <button | |
| onClick={() => setDeletingIndex(null)} | |
| className="text-xs text-slate-500 px-2 py-1 cursor-pointer hover:text-slate-700" | |
| > | |
| Batal | |
| </button> | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setDeletingIndex(i)} | |
| className="text-slate-400 hover:text-red-500 p-1 rounded hover:bg-red-50 transition-colors cursor-pointer" | |
| title="Hapus" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| )} | |
| </div> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |