Refactor: Implement clipboard paste for signatures
Implement direct clipboard paste functionality for signature ingestion, removing the need for a text box and submit button.
This commit is contained in:
@@ -1,233 +0,0 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Upload, Trash2 } from "lucide-react";
|
|
||||||
import { toast } from "@/hooks/use-toast";
|
|
||||||
import pb from "@/lib/pocketbase";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
interface Signature {
|
|
||||||
identifier: string;
|
|
||||||
type: string;
|
|
||||||
signame: string;
|
|
||||||
system: string;
|
|
||||||
sysid: string;
|
|
||||||
dangerous?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SignatureIngestProps {
|
|
||||||
system: string;
|
|
||||||
region: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SignatureIngest = ({ system, region }: SignatureIngestProps) => {
|
|
||||||
const [pasteData, setPasteData] = useState("");
|
|
||||||
const [cleanMode, setCleanMode] = useState(false);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const parseSignature = (text: string): Omit<Signature, 'system' | 'sysid'> | null => {
|
|
||||||
const parts = text.split('\t');
|
|
||||||
if (parts.length < 4) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
identifier: parts[0],
|
|
||||||
type: parts[2],
|
|
||||||
signame: parts[3],
|
|
||||||
dangerous: false // TODO: Implement dangerous signature detection
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSystemId = async (systemName: string): Promise<string> => {
|
|
||||||
const url = `https://evebase.site.quack-lab.dev/api/collections/regionview/records?filter=(sysname='${encodeURIComponent(systemName)}')`;
|
|
||||||
const response = await fetch(url);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.items && data.items.length > 0) {
|
|
||||||
return data.items[0].id;
|
|
||||||
}
|
|
||||||
throw new Error(`System ${systemName} not found`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSignature = async (signature: Signature): Promise<void> => {
|
|
||||||
// Check if signature already exists
|
|
||||||
const existingUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records?filter=(identifier='${signature.identifier}' && system='${signature.system}')`;
|
|
||||||
const existingResponse = await fetch(existingUrl);
|
|
||||||
const existingData = await existingResponse.json();
|
|
||||||
|
|
||||||
if (existingData.items && existingData.items.length > 0) {
|
|
||||||
// Update existing signature
|
|
||||||
const existingId = existingData.items[0].id;
|
|
||||||
const updateUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records/${existingId}`;
|
|
||||||
const updateResponse = await fetch(updateUrl, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
signame: signature.signame,
|
|
||||||
type: signature.type,
|
|
||||||
dangerous: signature.dangerous
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!updateResponse.ok) {
|
|
||||||
throw new Error(`Failed to update signature: ${updateResponse.status}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create new signature
|
|
||||||
const createUrl = 'https://evebase.site.quack-lab.dev/api/collections/sigview/records';
|
|
||||||
const createResponse = await fetch(createUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(signature)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!createResponse.ok) {
|
|
||||||
throw new Error(`Failed to create signature: ${createResponse.status}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSignature = async (signatureId: string): Promise<void> => {
|
|
||||||
const deleteUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records/${signatureId}`;
|
|
||||||
const response = await fetch(deleteUrl, { method: 'DELETE' });
|
|
||||||
|
|
||||||
if (!response.ok && response.status !== 204) {
|
|
||||||
throw new Error(`Failed to delete signature: ${response.status}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!pasteData.trim()) {
|
|
||||||
toast({
|
|
||||||
title: "No Data",
|
|
||||||
description: "Please paste signature data to submit.",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
const systemId = await getSystemId(system);
|
|
||||||
const lines = pasteData.trim().split('\n').filter(line => line.trim());
|
|
||||||
const parsedSignatures: Signature[] = [];
|
|
||||||
|
|
||||||
// Parse all signatures
|
|
||||||
for (const line of lines) {
|
|
||||||
const parsed = parseSignature(line);
|
|
||||||
if (parsed) {
|
|
||||||
parsedSignatures.push({
|
|
||||||
...parsed,
|
|
||||||
system,
|
|
||||||
sysid: systemId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedSignatures.length === 0) {
|
|
||||||
toast({
|
|
||||||
title: "No Valid Signatures",
|
|
||||||
description: "No valid signatures found in the pasted data.",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If clean mode is enabled, get existing signatures and delete ones not in the new list
|
|
||||||
if (cleanMode) {
|
|
||||||
const existingUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records?filter=(system='${encodeURIComponent(system)}')`;
|
|
||||||
const existingResponse = await fetch(existingUrl);
|
|
||||||
const existingData = await existingResponse.json();
|
|
||||||
|
|
||||||
const newIdentifiers = new Set(parsedSignatures.map(sig => sig.identifier));
|
|
||||||
const toDelete = existingData.items?.filter((item: any) => !newIdentifiers.has(item.identifier)) || [];
|
|
||||||
|
|
||||||
// Delete signatures not in the new list
|
|
||||||
for (const item of toDelete) {
|
|
||||||
await deleteSignature(item.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save all new/updated signatures
|
|
||||||
for (const signature of parsedSignatures) {
|
|
||||||
await saveSignature(signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate queries to refresh the data
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['signatures', system] });
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: `${parsedSignatures.length} signatures processed${cleanMode ? ' (clean mode)' : ''}.`
|
|
||||||
});
|
|
||||||
|
|
||||||
setPasteData("");
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to submit signatures:', error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error instanceof Error ? error.message : "Failed to submit signatures.",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaste = (e: React.ClipboardEvent) => {
|
|
||||||
const pastedText = e.clipboardData.getData('text');
|
|
||||||
setPasteData(prev => prev + (prev ? '\n' : '') + pastedText);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-slate-800/30 border-slate-700">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-white flex items-center gap-2">
|
|
||||||
<Upload className="h-5 w-5 text-blue-400" />
|
|
||||||
Ingest Signatures
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="signature-data" className="text-slate-200">
|
|
||||||
Paste signature data (Ctrl+V)
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id="signature-data"
|
|
||||||
placeholder="Paste signatures here (one per line, tab-separated)"
|
|
||||||
value={pasteData}
|
|
||||||
onChange={(e) => setPasteData(e.target.value)}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
className="bg-slate-700 border-slate-600 text-white placeholder:text-slate-400 min-h-[120px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
id="clean-mode"
|
|
||||||
checked={cleanMode}
|
|
||||||
onCheckedChange={setCleanMode}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="clean-mode" className="text-slate-200 flex items-center gap-2">
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
Clean mode (delete signatures not in list)
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting || !pasteData.trim()}
|
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Processing..." : "Submit Signatures"}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SignatureIngest;
|
|
@@ -1,31 +1,163 @@
|
|||||||
|
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import SystemTracker from "@/components/SystemTracker";
|
import SystemTracker from "@/components/SystemTracker";
|
||||||
import SignatureIngest from "@/components/SignatureIngest";
|
|
||||||
import RegionMap from "@/components/RegionMap";
|
import RegionMap from "@/components/RegionMap";
|
||||||
|
|
||||||
|
interface Signature {
|
||||||
|
identifier: string;
|
||||||
|
type: string;
|
||||||
|
signame: string;
|
||||||
|
system: string;
|
||||||
|
sysid: string;
|
||||||
|
dangerous?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const SystemView = () => {
|
const SystemView = () => {
|
||||||
const { system, region } = useParams();
|
const { system, region } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
if (!system) {
|
if (!system) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseSignature = (text: string): Omit<Signature, 'system' | 'sysid'> | null => {
|
||||||
|
const parts = text.split('\t');
|
||||||
|
if (parts.length < 4) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
identifier: parts[0],
|
||||||
|
type: parts[2],
|
||||||
|
signame: parts[3],
|
||||||
|
dangerous: false // TODO: Implement dangerous signature detection
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSystemId = async (systemName: string): Promise<string> => {
|
||||||
|
const url = `https://evebase.site.quack-lab.dev/api/collections/regionview/records?filter=(sysname='${encodeURIComponent(systemName)}')`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.items && data.items.length > 0) {
|
||||||
|
return data.items[0].id;
|
||||||
|
}
|
||||||
|
throw new Error(`System ${systemName} not found`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSignature = async (signature: Signature): Promise<void> => {
|
||||||
|
// Check if signature already exists
|
||||||
|
const existingUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records?filter=(identifier='${signature.identifier}' && system='${signature.system}')`;
|
||||||
|
const existingResponse = await fetch(existingUrl);
|
||||||
|
const existingData = await existingResponse.json();
|
||||||
|
|
||||||
|
if (existingData.items && existingData.items.length > 0) {
|
||||||
|
// Update existing signature
|
||||||
|
const existingId = existingData.items[0].id;
|
||||||
|
const updateUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records/${existingId}`;
|
||||||
|
const updateResponse = await fetch(updateUrl, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
signame: signature.signame,
|
||||||
|
type: signature.type,
|
||||||
|
dangerous: signature.dangerous
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updateResponse.ok) {
|
||||||
|
throw new Error(`Failed to update signature: ${updateResponse.status}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new signature
|
||||||
|
const createUrl = 'https://evebase.site.quack-lab.dev/api/collections/sigview/records';
|
||||||
|
const createResponse = await fetch(createUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(signature)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createResponse.ok) {
|
||||||
|
throw new Error(`Failed to create signature: ${createResponse.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = async (e: ClipboardEvent) => {
|
||||||
|
const pastedText = e.clipboardData?.getData('text');
|
||||||
|
if (!pastedText?.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const systemId = await getSystemId(system);
|
||||||
|
const lines = pastedText.trim().split('\n').filter(line => line.trim());
|
||||||
|
const parsedSignatures: Signature[] = [];
|
||||||
|
|
||||||
|
// Parse all signatures
|
||||||
|
for (const line of lines) {
|
||||||
|
const parsed = parseSignature(line);
|
||||||
|
if (parsed) {
|
||||||
|
parsedSignatures.push({
|
||||||
|
...parsed,
|
||||||
|
system,
|
||||||
|
sysid: systemId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedSignatures.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "No Valid Signatures",
|
||||||
|
description: "No valid signatures found in the pasted data.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all new/updated signatures
|
||||||
|
for (const signature of parsedSignatures) {
|
||||||
|
await saveSignature(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]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-white mb-2">System: {system}</h1>
|
<h1 className="text-4xl font-bold text-white mb-2">System: {system}</h1>
|
||||||
<p className="text-slate-300">Viewing signatures and regional overview</p>
|
<p className="text-slate-300">Viewing signatures and regional overview • Press Ctrl+V to paste signatures</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Main content - signatures and ingestion */}
|
{/* Main content - signatures */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
<SystemTracker system={system} />
|
<SystemTracker system={system} />
|
||||||
<SignatureIngest system={system} region={region || ''} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Regional overview map */}
|
{/* Regional overview map */}
|
||||||
|
Reference in New Issue
Block a user