Files
eve-signaler/frontend/src/components/SignatureListItem.tsx
PhatPhuckDave 22a7d1ad45 Implement note rules
So we may specify regex rules to map to notes
2025-10-05 15:12:17 +02:00

198 lines
6.4 KiB
TypeScript

import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Clock, AlertTriangle, Skull, Trash2 } from "lucide-react";
import { SigviewRecord as Signature } from "@/lib/pbtypes";
import { getSignatureMeta } from "@/hooks/useSignatureCategories";
import { SignatureEditModal } from "@/components/SignatureEditModal";
interface SignatureListItemProps {
signature: Signature;
onDelete?: (signatureId: string) => Promise<void>;
onUpdate?: (updatedSignature: Partial<Signature>) => Promise<void>;
}
export const SignatureListItem = ({ signature, onDelete, onUpdate }: SignatureListItemProps) => {
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const isOld = () => {
if (!signature.updated) return false;
const updatedTime = new Date(signature.updated);
if (isNaN(updatedTime.getTime())) return false; // Handle invalid date
const now = new Date();
const diffHours = (now.getTime() - updatedTime.getTime()) / (1000 * 60 * 60);
return diffHours > 3;
};
const formatDate = (dateStr: string | undefined) => {
if (!dateStr) return "Unknown";
const date = new Date(dateStr);
if (isNaN(date.getTime())) return "Invalid date"; // Handle invalid date
const now = new Date();
const diffMs = now.getTime() - date.getTime();
// Handle cases where the time difference is very small or negative
if (diffMs < 0) return "Just now";
const diffMinutes = Math.max(0, Math.floor(diffMs / (1000 * 60)));
const diffHours = Math.max(0, Math.floor(diffMs / (1000 * 60 * 60)));
if (diffMinutes === 0) {
return "Just now";
} else if (diffMinutes < 60) {
return `${diffMinutes}m ago`;
} else if (diffHours < 96) {
return `${diffHours}h ago`;
} else {
return date.toLocaleString();
}
};
const meta = getSignatureMeta(signature.type || "");
const isGasSite = signature.type?.toLowerCase().includes('gas');
const oldEntry = isOld();
const handleDelete = async () => {
if (onDelete) {
try {
await onDelete(signature.id);
} catch (error) {
console.error('Failed to delete signature:', error);
}
}
};
const handleUpdate = async (updatedSignature: Partial<Signature>) => {
if (onUpdate) {
try {
await onUpdate(updatedSignature);
} catch (error) {
console.error('Failed to update signature:', error);
throw error;
}
}
};
return (
<>
<div
className={`flex items-center justify-between p-4 border-b border-slate-700 hover:bg-slate-800/40 transition-colors cursor-pointer ${oldEntry ? "opacity-50" : ""
} ${isGasSite ? 'bg-emerald-900/40 border-emerald-500 shadow-[0_0_15px_rgba(16,185,129,0.5)] hover:shadow-[0_0_20px_rgba(16,185,129,0.7)]' : ''}`}
onClick={() => setIsEditModalOpen(true)}
>
<div className="flex items-center gap-4 flex-1">
{/* Type Badge - Most Important */}
<Badge
variant="outline"
className={`${meta.color} px-3 py-1 text-sm font-semibold flex items-center gap-2 min-w-[120px] justify-center`}
>
{signature.dangerous
? <Skull className="h-4 w-4 text-red-400 animate-pulse" />
: meta.icon}
{signature.type || "Unknown Type"}
</Badge>
{/* Signature Name and ID */}
<div className="flex-1 min-w-[200px]">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-slate-400 min-w-[60px]">{signature.identifier}</span>
<h3 className="text-white font-medium flex items-center gap-2">
{signature.signame || "Unnamed Signature"}
{signature.dangerous && (
<Badge variant="outline" className="bg-red-900/50 text-red-200 border-red-500 px-2 py-0.5 text-xs">
DANGEROUS
</Badge>
)}
</h3>
{signature.note && (
<div className="flex flex-wrap gap-1 ml-2">
{signature.note.split(';').filter(Boolean).map((note, index) => (
<Badge key={index} variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-2 py-0.5 text-sm font-semibold">
{note.trim()}
</Badge>
))}
</div>
)}
</div>
</div>
{/* Dates */}
<div className="flex flex-col gap-1 text-sm text-slate-400 min-w-[200px]">
{signature.updated && (
<div className="flex items-center gap-2">
{oldEntry && <AlertTriangle className="h-4 w-4 text-yellow-500" />}
<Clock className="h-4 w-4" />
<span>Updated: {formatDate(signature.updated)}</span>
</div>
)}
{signature.created && (
<div className="flex items-center gap-2 text-xs text-slate-500">
<span>Created: {formatDate(signature.created)}</span>
</div>
)}
</div>
</div>
{/* Delete Button */}
{onDelete && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-400 hover:bg-red-900/20 transition-colors"
onClick={(e) => e.stopPropagation()} // Prevent opening edit modal when clicking delete
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="bg-slate-800 border-slate-700">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">Delete Signature</AlertDialogTitle>
<AlertDialogDescription className="text-slate-300">
Are you sure you want to delete signature <span className="font-mono text-white">{signature.identifier}</span>?
<br />
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="bg-slate-700 border-slate-600 text-slate-200 hover:bg-slate-600">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-red-600 hover:bg-red-700 text-white"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
{/* Edit Modal */}
{onUpdate && (
<SignatureEditModal
signature={signature}
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
onSave={handleUpdate}
/>
)}
</>
);
};