Implement signature ingestion and cleanup
- Add functionality to paste and parse signatures from text input. - Implement a "clean" toggle to delete signatures not present in the pasted input. - Update routing to use /regions/<region>/<system> format.
This commit is contained in:
30
src/App.tsx
30
src/App.tsx
@@ -1,32 +1,30 @@
|
||||
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import Index from "./pages/Index";
|
||||
import RegionPage from "./pages/RegionPage";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import SystemView from "./pages/SystemView";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import "./App.css";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/regions/:region" element={<RegionPage />} />
|
||||
<Route path="/regions/:region/:system" element={<SystemView />} />
|
||||
<Route path="/systems/:system" element={<SystemView />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
<Toaster />
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
@@ -57,11 +57,7 @@ const fetchRegionData = async (regionName: string): Promise<SolarSystem[]> => {
|
||||
return systems;
|
||||
};
|
||||
|
||||
export const RegionMap: React.FC<RegionMapProps> = ({
|
||||
regionName,
|
||||
focusSystem,
|
||||
isCompact = false
|
||||
}) => {
|
||||
const RegionMap = ({ regionName, focusSystem, isCompact = false }: RegionMapProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 });
|
||||
const [isPanning, setIsPanning] = useState(false);
|
||||
@@ -135,8 +131,9 @@ export const RegionMap: React.FC<RegionMapProps> = ({
|
||||
}, [systems, focusSystem, isCompact]);
|
||||
|
||||
const handleSystemClick = (systemName: string) => {
|
||||
// Pass the region name when navigating to a system
|
||||
navigate(`/systems/${systemName}?region=${regionName}`);
|
||||
if (focusSystem === systemName) return;
|
||||
|
||||
navigate(`/regions/${regionName}/${systemName}`);
|
||||
};
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
@@ -378,3 +375,5 @@ export const RegionMap: React.FC<RegionMapProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionMap;
|
||||
|
233
src/components/SignatureIngest.tsx
Normal file
233
src/components/SignatureIngest.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
|
||||
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,3 +1,4 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
|
@@ -1,13 +1,12 @@
|
||||
|
||||
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import SystemTracker from "@/components/SystemTracker";
|
||||
import SignatureIngest from "@/components/SignatureIngest";
|
||||
import { RegionMap } from "@/components/RegionMap";
|
||||
|
||||
const SystemView = () => {
|
||||
const { system } = useParams();
|
||||
const { system, region } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const regionName = searchParams.get('region');
|
||||
|
||||
if (!system) {
|
||||
navigate("/");
|
||||
@@ -23,22 +22,23 @@ const SystemView = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Main content - signatures */}
|
||||
<div className="lg:col-span-2">
|
||||
{/* Main content - signatures and ingestion */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<SystemTracker system={system} />
|
||||
<SignatureIngest system={system} region={region || ''} />
|
||||
</div>
|
||||
|
||||
{/* Regional overview map */}
|
||||
<div className="lg:col-span-1">
|
||||
{regionName ? (
|
||||
{region ? (
|
||||
<div className="h-96 border border-purple-500/30 rounded-lg overflow-hidden">
|
||||
<div className="p-2 border-b border-purple-500/30 bg-black/20 backdrop-blur-sm">
|
||||
<h3 className="text-white text-sm font-semibold">{regionName} Region</h3>
|
||||
<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="h-[calc(100%-3rem)]">
|
||||
<RegionMap
|
||||
regionName={regionName}
|
||||
regionName={region}
|
||||
focusSystem={system}
|
||||
isCompact={true}
|
||||
/>
|
||||
|
Reference in New Issue
Block a user