220 lines
8.0 KiB
TypeScript
220 lines
8.0 KiB
TypeScript
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { useEffect, useState } from "react";
|
|
import { toast } from "@/hooks/use-toast";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { SystemTracker } from "@/components/SystemTracker";
|
|
import { RegionMap } from "@/components/RegionMap";
|
|
import { CleanModeToggle } from "@/components/CleanModeToggle";
|
|
import { Header } from "@/components/Header";
|
|
import { parseSignature, parseScannedPercentage } from "@/utils/signatureParser";
|
|
import { getSystemId } from "@/utils/systemApi";
|
|
import pb from "@/lib/pocketbase";
|
|
import { SigviewRecord as Signature } from "@/lib/pbtypes";
|
|
|
|
export const SystemView = () => {
|
|
const { system, region } = useParams();
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const [cleanMode, setCleanMode] = useState(false);
|
|
|
|
if (!system) {
|
|
navigate("/");
|
|
return null;
|
|
}
|
|
|
|
const saveSignature = async (signature: Signature): Promise<void> => {
|
|
try {
|
|
// Check if signature already exists
|
|
let existingRecord: Signature | null = null;
|
|
try {
|
|
existingRecord = await pb.collection('signature').getFirstListItem(`identifier='${signature.identifier}' && system='${signature.system}'`);
|
|
} catch (error) {
|
|
console.log(`Signature ${signature.identifier} not found`);
|
|
}
|
|
|
|
const newScannedPercentage = parseScannedPercentage(signature.scanned);
|
|
|
|
if (existingRecord) {
|
|
// Update existing signature only if new scan percentage is higher
|
|
const existingScannedPercentage = parseScannedPercentage(existingRecord.scanned);
|
|
if (newScannedPercentage >= existingScannedPercentage) {
|
|
await pb.collection('signature').update(existingRecord.id, {
|
|
name: signature.signame,
|
|
type: signature.type,
|
|
dangerous: signature.dangerous,
|
|
scanned: signature.scanned
|
|
});
|
|
console.log(`Updated signature ${signature.identifier}: ${existingScannedPercentage}% -> ${newScannedPercentage}%`);
|
|
} else {
|
|
console.log(`Skipped updating signature ${signature.identifier}: new scan ${newScannedPercentage}% is not better than existing ${existingScannedPercentage}%`);
|
|
}
|
|
} else {
|
|
// Create new signature
|
|
await pb.collection('signature').create({
|
|
system: signature.system,
|
|
identifier: signature.identifier,
|
|
name: signature.signame,
|
|
type: signature.type,
|
|
dangerous: signature.dangerous,
|
|
scanned: signature.scanned
|
|
});
|
|
console.log(`Created new signature ${signature.identifier} with ${newScannedPercentage}% scan`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save signature:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const deleteSignature = async (signatureId: string): Promise<void> => {
|
|
try {
|
|
await pb.collection('signature').delete(signatureId);
|
|
} catch (error) {
|
|
console.error('Failed to delete signature:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const handlePaste = async (e: ClipboardEvent) => {
|
|
const pastedText = e.clipboardData?.getData('text');
|
|
if (!pastedText?.trim()) return;
|
|
|
|
const wasCleanMode = cleanMode;
|
|
|
|
try {
|
|
const systemId = await getSystemId(system);
|
|
const lines = pastedText.trim().split('\n').filter(line => line.trim());
|
|
const parsedSignatures: Omit<Signature, 'id'>[] = [];
|
|
|
|
// Parse all signatures
|
|
for (const line of lines) {
|
|
const parsed = parseSignature(line);
|
|
if (parsed) {
|
|
parsedSignatures.push({
|
|
...parsed,
|
|
system: systemId,
|
|
sysid: systemId,
|
|
identifier: parsed.identifier
|
|
});
|
|
}
|
|
}
|
|
|
|
if (parsedSignatures.length === 0) {
|
|
toast({
|
|
title: "No Valid Signatures",
|
|
description: "No valid signatures found in the pasted data. Signatures must follow the format: ABC-123.",
|
|
variant: "destructive"
|
|
});
|
|
return;
|
|
}
|
|
|
|
// If clean mode is enabled, delete signatures not in the pasted list
|
|
if (wasCleanMode) {
|
|
const existingSignatures = await pb.collection('signature').getFullList({
|
|
filter: `system='${systemId}'`
|
|
});
|
|
|
|
const pastedIdentifiers = new Set(parsedSignatures.map(sig => sig.identifier));
|
|
const signaturesToDelete = existingSignatures.filter(sig => !pastedIdentifiers.has(sig.identifier));
|
|
|
|
for (const sig of signaturesToDelete) {
|
|
await deleteSignature(sig.id);
|
|
}
|
|
|
|
if (signaturesToDelete.length > 0) {
|
|
console.log(`Deleted ${signaturesToDelete.length} signatures not in pasted data`);
|
|
}
|
|
|
|
// Turn off clean mode after use
|
|
setCleanMode(false);
|
|
}
|
|
|
|
// Save all new/updated signatures
|
|
for (const signature of parsedSignatures) {
|
|
await saveSignature(signature as Signature);
|
|
}
|
|
|
|
// Invalidate queries to refresh the data
|
|
queryClient.invalidateQueries({ queryKey: ['signatures', system] });
|
|
|
|
toast({
|
|
title: "Success",
|
|
description: `${parsedSignatures.length} signatures processed.`
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Failed to process signatures:', error);
|
|
toast({
|
|
title: "Error",
|
|
description: error instanceof Error ? error.message : "Failed to process signatures.",
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
document.addEventListener('paste', handlePaste);
|
|
return () => {
|
|
document.removeEventListener('paste', handlePaste);
|
|
};
|
|
}, [system, cleanMode]);
|
|
|
|
const breadcrumbs = [
|
|
{ label: "Universe", path: "/" },
|
|
...(region ? [{ label: region, path: `/regions/${region}` }] : []),
|
|
{ label: system }
|
|
];
|
|
|
|
return (
|
|
<div className="h-screen w-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-hidden">
|
|
<div className="h-full flex flex-col">
|
|
<Header title={`System: ${system}`} breadcrumbs={breadcrumbs} />
|
|
|
|
{/* Controls */}
|
|
<div className="flex-shrink-0 px-4 py-3 border-b border-purple-500/20">
|
|
<div className="flex items-center justify-center gap-4 text-slate-300">
|
|
<p>Press Ctrl+V to paste signatures</p>
|
|
<CleanModeToggle cleanMode={cleanMode} onToggle={setCleanMode} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-hidden px-4 pb-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-full pt-6">
|
|
{/* Main content - signatures */}
|
|
<div className="lg:col-span-2 space-y-6 overflow-y-auto">
|
|
<SystemTracker system={system} />
|
|
</div>
|
|
|
|
{/* Regional overview map */}
|
|
<div className="lg:col-span-1 flex flex-col">
|
|
{region ? (
|
|
<div className="h-96 border border-purple-500/30 rounded-lg overflow-hidden flex flex-col">
|
|
<div className="flex-shrink-0 p-2 border-b border-purple-500/30 bg-black/20 backdrop-blur-sm">
|
|
<h3 className="text-white text-sm font-semibold">{region} Region</h3>
|
|
<p className="text-purple-200 text-xs">Click systems to navigate • Current: {system}</p>
|
|
</div>
|
|
<div className="flex-1 min-h-0">
|
|
<RegionMap
|
|
regionName={region}
|
|
focusSystem={system}
|
|
isCompact={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="bg-black/20 backdrop-blur-sm border border-purple-500/30 rounded-lg p-4 h-96 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="text-white text-sm">No region information</div>
|
|
<div className="text-purple-200 text-xs mt-1">Navigate from a region map to see regional overview</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|