feat: Categorize and toggle signatures
Refactor SystemView to display signatures in categorized, collapsible sections. Implement user preference persistence for category visibility using local storage.
This commit is contained in:
99
src/components/SignatureCategories.tsx
Normal file
99
src/components/SignatureCategories.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
|
import { ChevronDown, ChevronRight, Zap, Shield, Coins, HelpCircle, Pickaxe, Gauge } from "lucide-react";
|
||||||
|
import { SignatureListItem } from "@/components/SignatureListItem";
|
||||||
|
import { SignatureCategory } from "@/hooks/useSignatureCategories";
|
||||||
|
|
||||||
|
interface SignatureCategoriesProps {
|
||||||
|
categories: SignatureCategory[];
|
||||||
|
onToggleCategory: (categoryId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryIcon = (categoryId: string) => {
|
||||||
|
switch (categoryId) {
|
||||||
|
case 'combat':
|
||||||
|
return <Zap className="h-4 w-4 text-red-400" />;
|
||||||
|
case 'data_relic':
|
||||||
|
return <Shield className="h-4 w-4 text-blue-400" />;
|
||||||
|
case 'gas':
|
||||||
|
return <Gauge className="h-4 w-4 text-green-400" />;
|
||||||
|
case 'ore':
|
||||||
|
return <Pickaxe className="h-4 w-4 text-yellow-400" />;
|
||||||
|
case 'wormhole':
|
||||||
|
return <Coins className="h-4 w-4 text-purple-400" />;
|
||||||
|
default:
|
||||||
|
return <HelpCircle className="h-4 w-4 text-slate-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryColor = (categoryId: string) => {
|
||||||
|
switch (categoryId) {
|
||||||
|
case 'combat':
|
||||||
|
return 'text-red-400 border-red-600';
|
||||||
|
case 'data_relic':
|
||||||
|
return 'text-blue-400 border-blue-600';
|
||||||
|
case 'gas':
|
||||||
|
return 'text-green-400 border-green-600';
|
||||||
|
case 'ore':
|
||||||
|
return 'text-yellow-400 border-yellow-600';
|
||||||
|
case 'wormhole':
|
||||||
|
return 'text-purple-400 border-purple-600';
|
||||||
|
default:
|
||||||
|
return 'text-slate-400 border-slate-600';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SignatureCategories = ({ categories, onToggleCategory }: SignatureCategoriesProps) => {
|
||||||
|
if (categories.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-slate-800/30 border-slate-700">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<div className="text-slate-400">No signatures found</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<Card key={category.id} className="bg-slate-800/30 border-slate-700">
|
||||||
|
<Collapsible open={category.isVisible} onOpenChange={() => onToggleCategory(category.id)}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<CardHeader className="cursor-pointer hover:bg-slate-800/50 transition-colors">
|
||||||
|
<CardTitle className="text-white flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{category.isVisible ? (
|
||||||
|
<ChevronDown className="h-4 w-4 text-slate-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 text-slate-400" />
|
||||||
|
)}
|
||||||
|
{getCategoryIcon(category.id)}
|
||||||
|
<span>{category.name}</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`bg-slate-700 border-slate-600 ${getCategoryColor(category.id)}`}
|
||||||
|
>
|
||||||
|
{category.signatures.length}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="divide-y divide-slate-700">
|
||||||
|
{category.signatures.map((signature) => (
|
||||||
|
<SignatureListItem key={signature.id} signature={signature} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,7 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, AlertCircle, Radar } from "lucide-react";
|
import { RefreshCw, AlertCircle, Radar } from "lucide-react";
|
||||||
import { SignatureListItem } from "@/components/SignatureListItem";
|
import { SignatureCategories } from "@/components/SignatureCategories";
|
||||||
|
import { useSignatureCategories } from "@/hooks/useSignatureCategories";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
import pb from "@/lib/pocketbase";
|
import pb from "@/lib/pocketbase";
|
||||||
|
|
||||||
@@ -59,6 +60,8 @@ export const SystemTracker = ({ system }: SystemTrackerProps) => {
|
|||||||
return dateB.localeCompare(dateA);
|
return dateB.localeCompare(dateA);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { categories, toggleCategoryVisibility } = useSignatureCategories(sortedSignatures);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* System Status Card */}
|
{/* System Status Card */}
|
||||||
@@ -96,37 +99,6 @@ export const SystemTracker = ({ system }: SystemTrackerProps) => {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Signatures Display */}
|
|
||||||
{system && !signaturesLoading && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{sortedSignatures.length === 0 ? (
|
|
||||||
<Card className="bg-slate-800/30 border-slate-700">
|
|
||||||
<CardContent className="pt-6 text-center">
|
|
||||||
<div className="text-slate-400">No signatures found for {system}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card className="bg-slate-800/30 border-slate-700">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-white flex items-center justify-between">
|
|
||||||
<span>Signatures</span>
|
|
||||||
<Badge variant="outline" className="bg-slate-700 text-slate-200 border-slate-600">
|
|
||||||
{sortedSignatures.length} Total
|
|
||||||
</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<div className="divide-y divide-slate-700">
|
|
||||||
{sortedSignatures.map((signature) => (
|
|
||||||
<SignatureListItem key={signature.id} signature={signature} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Loading State */}
|
{/* Loading State */}
|
||||||
{signaturesLoading && system && (
|
{signaturesLoading && system && (
|
||||||
<Card className="bg-slate-800/30 border-slate-700">
|
<Card className="bg-slate-800/30 border-slate-700">
|
||||||
@@ -143,6 +115,14 @@ export const SystemTracker = ({ system }: SystemTrackerProps) => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Signature Categories */}
|
||||||
|
{system && !signaturesLoading && (
|
||||||
|
<SignatureCategories
|
||||||
|
categories={categories}
|
||||||
|
onToggleCategory={toggleCategoryVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
130
src/hooks/useSignatureCategories.ts
Normal file
130
src/hooks/useSignatureCategories.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { SigviewRecord as Signature } from "@/lib/pbtypes";
|
||||||
|
|
||||||
|
export interface SignatureCategory {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
signatures: Signature[];
|
||||||
|
isVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATEGORY_PREFERENCES_KEY = 'signature-category-preferences';
|
||||||
|
|
||||||
|
export const categorizeSignatures = (signatures: Signature[]): Record<string, Signature[]> => {
|
||||||
|
const categories: Record<string, Signature[]> = {
|
||||||
|
unknown: [],
|
||||||
|
combat: [],
|
||||||
|
data_relic: [],
|
||||||
|
gas: [],
|
||||||
|
ore: [],
|
||||||
|
wormhole: []
|
||||||
|
};
|
||||||
|
|
||||||
|
signatures.forEach(signature => {
|
||||||
|
const type = signature.type?.toLowerCase() || '';
|
||||||
|
const name = signature.signame?.toLowerCase() || '';
|
||||||
|
|
||||||
|
if (!type || type === '') {
|
||||||
|
categories.unknown.push(signature);
|
||||||
|
} else if (type.includes('combat') || type.includes('den') || type.includes('rally')) {
|
||||||
|
categories.combat.push(signature);
|
||||||
|
} else if (type.includes('data') || type.includes('relic') || type.includes('exploration')) {
|
||||||
|
categories.data_relic.push(signature);
|
||||||
|
} else if (type.includes('gas') || name.includes('gas')) {
|
||||||
|
categories.gas.push(signature);
|
||||||
|
} else if (type.includes('ore') || type.includes('mining') || name.includes('ore')) {
|
||||||
|
categories.ore.push(signature);
|
||||||
|
} else if (type.includes('wormhole') || name.includes('wormhole') || type.includes('k162')) {
|
||||||
|
categories.wormhole.push(signature);
|
||||||
|
} else {
|
||||||
|
categories.unknown.push(signature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSignatureCategories = (signatures: Signature[]) => {
|
||||||
|
const [categoryVisibility, setCategoryVisibility] = useState<Record<string, boolean>>({
|
||||||
|
unknown: true,
|
||||||
|
combat: true,
|
||||||
|
data_relic: true,
|
||||||
|
gas: true,
|
||||||
|
ore: true,
|
||||||
|
wormhole: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load preferences from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem(CATEGORY_PREFERENCES_KEY);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const preferences = JSON.parse(saved);
|
||||||
|
setCategoryVisibility(prev => ({ ...prev, ...preferences }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse category preferences:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Save preferences to localStorage when they change
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(CATEGORY_PREFERENCES_KEY, JSON.stringify(categoryVisibility));
|
||||||
|
}, [categoryVisibility]);
|
||||||
|
|
||||||
|
const categories = useMemo(() => {
|
||||||
|
const categorized = categorizeSignatures(signatures);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'unknown',
|
||||||
|
name: 'Unknown Sites',
|
||||||
|
signatures: categorized.unknown,
|
||||||
|
isVisible: categoryVisibility.unknown
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'combat',
|
||||||
|
name: 'Combat Sites',
|
||||||
|
signatures: categorized.combat,
|
||||||
|
isVisible: categoryVisibility.combat
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data_relic',
|
||||||
|
name: 'Data/Relic Sites',
|
||||||
|
signatures: categorized.data_relic,
|
||||||
|
isVisible: categoryVisibility.data_relic
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gas',
|
||||||
|
name: 'Gas Sites',
|
||||||
|
signatures: categorized.gas,
|
||||||
|
isVisible: categoryVisibility.gas
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ore',
|
||||||
|
name: 'Ore Sites',
|
||||||
|
signatures: categorized.ore,
|
||||||
|
isVisible: categoryVisibility.ore
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wormhole',
|
||||||
|
name: 'Wormholes',
|
||||||
|
signatures: categorized.wormhole,
|
||||||
|
isVisible: categoryVisibility.wormhole
|
||||||
|
}
|
||||||
|
].filter(category => category.signatures.length > 0);
|
||||||
|
}, [signatures, categoryVisibility]);
|
||||||
|
|
||||||
|
const toggleCategoryVisibility = (categoryId: string) => {
|
||||||
|
setCategoryVisibility(prev => ({
|
||||||
|
...prev,
|
||||||
|
[categoryId]: !prev[categoryId]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
categories,
|
||||||
|
toggleCategoryVisibility
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user