Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
import { useState, ReactNode, useMemo } from 'react';
import {
Settings, X, Palette, RefreshCw, Maximize2, Minimize2, Eye, EyeOff,
Bell, Volume2, VolumeX, ListFilter, Hash, Zap, MapPin, Clock,
BarChart3, TrendingUp, AlertTriangle, Globe, Rss
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useWidgetSettings, generateWidgetId, WidgetConfig } from '@/hooks/useWidgetSettings';
// Re-export WidgetConfig for backwards compatibility
export type { WidgetConfig };
// Widget-specific settings
export interface NewsWidgetSettings {
itemCount: number;
showSeverityBadge: boolean;
sources: string[];
autoScroll: boolean;
}
export interface AlertWidgetSettings {
showCritical: boolean;
showWarning: boolean;
showInfo: boolean;
soundEnabled: boolean;
maxAlerts: number;
}
export interface MetricsWidgetSettings {
showChange: boolean;
compactMode: boolean;
animateValues: boolean;
decimalPlaces: number;
}
export interface MapWidgetSettings {
animationSpeed: 'slow' | 'normal' | 'fast';
showLabels: boolean;
hotspotSize: 'small' | 'medium' | 'large';
pulseEffect: boolean;
}
export interface RadarWidgetSettings {
scanSpeed: number;
showGrid: boolean;
threatHighlight: boolean;
radarStyle: 'classic' | 'modern';
}
export interface EventWidgetSettings {
maxEvents: number;
showTimestamps: boolean;
filterByType: string[];
highlightNew: boolean;
}
export interface StatisticsWidgetSettings {
showPercentage: boolean;
barStyle: 'filled' | 'gradient' | 'striped';
sortBy: 'value' | 'name';
animateBars: boolean;
}
export interface GlobalWidgetSettings {
showTrend: boolean;
sortByThreats: boolean;
highlightTop: boolean;
regionStyle: 'list' | 'compact';
}
export type WidgetSpecificSettings =
| NewsWidgetSettings
| AlertWidgetSettings
| MetricsWidgetSettings
| MapWidgetSettings
| RadarWidgetSettings
| EventWidgetSettings
| StatisticsWidgetSettings
| GlobalWidgetSettings
| Record<string, any>;
interface ConfigurableWidgetProps {
name: string;
icon: ReactNode;
widgetType?: string;
children: ((config: WidgetConfig, settings: any) => ReactNode) | ReactNode;
defaultConfig?: Partial<WidgetConfig>;
className?: string;
}
const colorOptions = [
{ value: 'primary', label: 'Cyan', class: 'bg-primary' },
{ value: 'accent', label: 'Teal', class: 'bg-accent' },
{ value: 'destructive', label: 'Red', class: 'bg-destructive' },
{ value: 'orange', label: 'Orange', class: 'bg-orange-400' },
];
const defaultBaseConfig: WidgetConfig = {
refreshRate: 30,
accentColor: 'primary',
showHeader: true,
expanded: false,
opacity: 100,
};
// Default settings for each widget type
const getDefaultSettings = (widgetType?: string): WidgetSpecificSettings => {
switch (widgetType) {
case 'news':
return { itemCount: 4, showSeverityBadge: true, sources: ['all'], autoScroll: false };
case 'alerts':
return { showCritical: true, showWarning: true, showInfo: true, soundEnabled: false, maxAlerts: 5 };
case 'metrics':
return { showChange: true, compactMode: false, animateValues: true, decimalPlaces: 0 };
case 'threatmap':
case 'map':
return { animationSpeed: 'normal', showLabels: true, hotspotSize: 'medium', pulseEffect: true };
case 'radar':
return { scanSpeed: 3, showGrid: true, threatHighlight: true, radarStyle: 'classic' };
case 'events':
return { maxEvents: 5, showTimestamps: true, filterByType: ['all'], highlightNew: true };
case 'statistics':
return { showPercentage: true, barStyle: 'filled', sortBy: 'value', animateBars: true };
case 'global':
return { showTrend: true, sortByThreats: true, highlightTop: true, regionStyle: 'list' };
default:
return {};
}
};
const ConfigurableWidget = ({
name,
icon,
widgetType,
children,
defaultConfig,
className
}: ConfigurableWidgetProps) => {
const [showConfig, setShowConfig] = useState(false);
const [activeTab, setActiveTab] = useState('general');
// Generate stable widget ID for localStorage
const widgetId = useMemo(() => generateWidgetId(name, widgetType), [name, widgetType]);
// Memoize default values
const mergedDefaultConfig = useMemo(() => ({
...defaultBaseConfig,
...defaultConfig,
}), [defaultConfig]);
const defaultSettings = useMemo(() => getDefaultSettings(widgetType), [widgetType]);
// Use persistent settings hook
const { config, settings, setConfig, setSettings, resetToDefaults } = useWidgetSettings({
widgetId,
defaultConfig: mergedDefaultConfig,
defaultSettings,
});
const updateConfig = (key: keyof WidgetConfig, value: any) => {
setConfig({ [key]: value });
};
const updateSettings = (key: string, value: any) => {
setSettings({ [key]: value });
};
const accentColorClass = {
primary: 'text-primary border-primary/50',
accent: 'text-accent border-accent/50',
destructive: 'text-destructive border-destructive/50',
orange: 'text-orange-400 border-orange-400/50',
}[config.accentColor];
const accentBgClass = {
primary: 'bg-primary/20',
accent: 'bg-accent/20',
destructive: 'bg-destructive/20',
orange: 'bg-orange-400/20',
}[config.accentColor];
// Widget-specific settings panels
const renderWidgetSpecificSettings = () => {
switch (widgetType) {
case 'news':
const newsSettings = settings as NewsWidgetSettings;
return (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Hash className="w-3 h-3" />
Antal nyheder
</label>
<Slider
value={[newsSettings.itemCount]}
onValueChange={([v]) => updateSettings('itemCount', v)}
min={2}
max={10}
step={1}
className="w-full"
/>
<span className="text-xs text-muted-foreground">{newsSettings.itemCount} items</span>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<AlertTriangle className="w-3 h-3" />
Vis severity badge
</label>
<Switch
checked={newsSettings.showSeverityBadge}
onCheckedChange={(v) => updateSettings('showSeverityBadge', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Rss className="w-3 h-3" />
Auto-scroll
</label>
<Switch
checked={newsSettings.autoScroll}
onCheckedChange={(v) => updateSettings('autoScroll', v)}
/>
</div>
</div>
);
case 'alerts':
const alertSettings = settings as AlertWidgetSettings;
return (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<ListFilter className="w-3 h-3" />
Filtrer alerts
</label>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-destructive">Critical</span>
<Switch
checked={alertSettings.showCritical}
onCheckedChange={(v) => updateSettings('showCritical', v)}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-orange-400">Warning</span>
<Switch
checked={alertSettings.showWarning}
onCheckedChange={(v) => updateSettings('showWarning', v)}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-primary">Info</span>
<Switch
checked={alertSettings.showInfo}
onCheckedChange={(v) => updateSettings('showInfo', v)}
/>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
{alertSettings.soundEnabled ? <Volume2 className="w-3 h-3" /> : <VolumeX className="w-3 h-3" />}
Lyd notifikationer
</label>
<Switch
checked={alertSettings.soundEnabled}
onCheckedChange={(v) => updateSettings('soundEnabled', v)}
/>
</div>
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Hash className="w-3 h-3" />
Max antal alerts
</label>
<Slider
value={[alertSettings.maxAlerts]}
onValueChange={([v]) => updateSettings('maxAlerts', v)}
min={3}
max={15}
step={1}
className="w-full"
/>
<span className="text-xs text-muted-foreground">{alertSettings.maxAlerts} alerts</span>
</div>
</div>
);
case 'metrics':
const metricsSettings = settings as MetricsWidgetSettings;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<TrendingUp className="w-3 h-3" />
Vis ændring
</label>
<Switch
checked={metricsSettings.showChange}
onCheckedChange={(v) => updateSettings('showChange', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Minimize2 className="w-3 h-3" />
Kompakt visning
</label>
<Switch
checked={metricsSettings.compactMode}
onCheckedChange={(v) => updateSettings('compactMode', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Zap className="w-3 h-3" />
Animér værdier
</label>
<Switch
checked={metricsSettings.animateValues}
onCheckedChange={(v) => updateSettings('animateValues', v)}
/>
</div>
<div className="space-y-2">
<label className="text-xs text-muted-foreground">Decimaler</label>
<Select
value={String(metricsSettings.decimalPlaces)}
onValueChange={(v) => updateSettings('decimalPlaces', Number(v))}
>
<SelectTrigger className="bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border z-50">
<SelectItem value="0">0 decimaler</SelectItem>
<SelectItem value="1">1 decimal</SelectItem>
<SelectItem value="2">2 decimaler</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
case 'threatmap':
case 'map':
const mapSettings = settings as MapWidgetSettings;
return (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Clock className="w-3 h-3" />
Animations hastighed
</label>
<Select
value={mapSettings.animationSpeed}
onValueChange={(v) => updateSettings('animationSpeed', v)}
>
<SelectTrigger className="bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border z-50">
<SelectItem value="slow">Langsom</SelectItem>
<SelectItem value="normal">Normal</SelectItem>
<SelectItem value="fast">Hurtig</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<MapPin className="w-3 h-3" />
Hotspot størrelse
</label>
<Select
value={mapSettings.hotspotSize}
onValueChange={(v) => updateSettings('hotspotSize', v)}
>
<SelectTrigger className="bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border z-50">
<SelectItem value="small">Lille</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="large">Stor</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Eye className="w-3 h-3" />
Vis labels
</label>
<Switch
checked={mapSettings.showLabels}
onCheckedChange={(v) => updateSettings('showLabels', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Zap className="w-3 h-3" />
Pulse effekt
</label>
<Switch
checked={mapSettings.pulseEffect}
onCheckedChange={(v) => updateSettings('pulseEffect', v)}
/>
</div>
</div>
);
case 'radar':
const radarSettings = settings as RadarWidgetSettings;
return (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<RefreshCw className="w-3 h-3" />
Scan hastighed: {radarSettings.scanSpeed}s
</label>
<Slider
value={[radarSettings.scanSpeed]}
onValueChange={([v]) => updateSettings('scanSpeed', v)}
min={1}
max={10}
step={1}
className="w-full"
/>
</div>
<div className="space-y-2">
<label className="text-xs text-muted-foreground">Radar stil</label>
<Select
value={radarSettings.radarStyle}
onValueChange={(v) => updateSettings('radarStyle', v)}
>
<SelectTrigger className="bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border z-50">
<SelectItem value="classic">Klassisk</SelectItem>
<SelectItem value="modern">Moderne</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
Vis grid
</label>
<Switch
checked={radarSettings.showGrid}
onCheckedChange={(v) => updateSettings('showGrid', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<AlertTriangle className="w-3 h-3" />
Highlight trusler
</label>
<Switch
checked={radarSettings.threatHighlight}
onCheckedChange={(v) => updateSettings('threatHighlight', v)}
/>
</div>
</div>
);
case 'statistics':
const statsSettings = settings as StatisticsWidgetSettings;
return (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<BarChart3 className="w-3 h-3" />
Bar stil
</label>
<Select
value={statsSettings.barStyle}
onValueChange={(v) => updateSettings('barStyle', v)}
>
<SelectTrigger className="bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border z-50">
<SelectItem value="filled">Fyldt</SelectItem>
<SelectItem value="gradient">Gradient</SelectItem>
<SelectItem value="striped">Stribet</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
Vis procent
</label>
<Switch
checked={statsSettings.showPercentage}
onCheckedChange={(v) => updateSettings('showPercentage', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Zap className="w-3 h-3" />
Animér bars
</label>
<Switch
checked={statsSettings.animateBars}
onCheckedChange={(v) => updateSettings('animateBars', v)}
/>
</div>
</div>
);
case 'global':
const globalSettings = settings as GlobalWidgetSettings;
return (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Globe className="w-3 h-3" />
Visningsstil
</label>
<Select
value={globalSettings.regionStyle}
onValueChange={(v) => updateSettings('regionStyle', v)}
>
<SelectTrigger className="bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border z-50">
<SelectItem value="list">Liste</SelectItem>
<SelectItem value="compact">Kompakt</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<TrendingUp className="w-3 h-3" />
Vis trend
</label>
<Switch
checked={globalSettings.showTrend}
onCheckedChange={(v) => updateSettings('showTrend', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
Sortér efter trusler
</label>
<Switch
checked={globalSettings.sortByThreats}
onCheckedChange={(v) => updateSettings('sortByThreats', v)}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
Highlight top region
</label>
<Switch
checked={globalSettings.highlightTop}
onCheckedChange={(v) => updateSettings('highlightTop', v)}
/>
</div>
</div>
);
default:
return (
<p className="text-xs text-muted-foreground">
Ingen specifikke indstillinger for denne widget type.
</p>
);
}
};
return (
<div
className={cn(
"relative bg-card/80 backdrop-blur-sm border overflow-hidden transition-all duration-300",
config.expanded ? "col-span-2 row-span-2" : "",
accentColorClass.split(' ')[1],
className
)}
style={{ opacity: config.opacity / 100 }}
>
{/* Header */}
{config.showHeader && (
<div className={cn(
"flex items-center gap-2 px-4 py-2 border-b border-border/50",
accentBgClass
)}>
<div className={cn("w-2 h-2 rounded-full animate-pulse", {
'bg-primary': config.accentColor === 'primary',
'bg-accent': config.accentColor === 'accent',
'bg-destructive': config.accentColor === 'destructive',
'bg-orange-400': config.accentColor === 'orange',
})} />
<span className={cn("font-display text-xs uppercase tracking-wider", accentColorClass.split(' ')[0])}>
{name}
</span>
<div className="flex-1" />
<span className="font-mono text-xs text-muted-foreground">
{config.refreshRate}s
</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => updateConfig('expanded', !config.expanded)}
>
{config.expanded ? (
<Minimize2 className="w-3 h-3" />
) : (
<Maximize2 className="w-3 h-3" />
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => setShowConfig(!showConfig)}
>
<Settings className={cn("w-3 h-3 transition-transform", showConfig && "rotate-90")} />
</Button>
</div>
)}
{/* Config Panel with Tabs */}
{showConfig && (
<div className="absolute inset-0 bg-background/95 backdrop-blur-sm z-20 p-4 overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<span className="font-display text-sm uppercase text-primary">Widget Settings</span>
<Button variant="ghost" size="sm" onClick={() => setShowConfig(false)}>
<X className="w-4 h-4" />
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-secondary/30">
<TabsTrigger value="general" className="text-xs">Generelt</TabsTrigger>
<TabsTrigger value="specific" className="text-xs">Widget</TabsTrigger>
</TabsList>
<TabsContent value="general" className="mt-4 space-y-4">
{/* Accent Color */}
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Palette className="w-3 h-3" />
Accent Farve
</label>
<div className="flex gap-2">
{colorOptions.map((color) => (
<button
key={color.value}
onClick={() => updateConfig('accentColor', color.value)}
className={cn(
"w-8 h-8 rounded-full transition-all",
color.class,
config.accentColor === color.value
? "ring-2 ring-offset-2 ring-offset-background ring-foreground scale-110"
: "opacity-60 hover:opacity-100"
)}
title={color.label}
/>
))}
</div>
</div>
{/* Refresh Rate */}
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<RefreshCw className="w-3 h-3" />
Opdateringsrate: {config.refreshRate}s
</label>
<Select
value={String(config.refreshRate)}
onValueChange={(v) => updateConfig('refreshRate', Number(v))}
>
<SelectTrigger className="bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border z-50">
<SelectItem value="5">5 sekunder</SelectItem>
<SelectItem value="15">15 sekunder</SelectItem>
<SelectItem value="30">30 sekunder</SelectItem>
<SelectItem value="60">1 minut</SelectItem>
<SelectItem value="300">5 minutter</SelectItem>
</SelectContent>
</Select>
</div>
{/* Opacity */}
<div className="space-y-2">
<label className="text-xs text-muted-foreground flex items-center gap-2">
<Eye className="w-3 h-3" />
Gennemsigtighed: {config.opacity}%
</label>
<Slider
value={[config.opacity]}
onValueChange={([v]) => updateConfig('opacity', v)}
min={40}
max={100}
step={10}
className="w-full"
/>
</div>
{/* Show Header Toggle */}
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground flex items-center gap-2">
{config.showHeader ? <Eye className="w-3 h-3" /> : <EyeOff className="w-3 h-3" />}
Vis Header
</label>
<Switch
checked={config.showHeader}
onCheckedChange={(v) => updateConfig('showHeader', v)}
/>
</div>
</TabsContent>
<TabsContent value="specific" className="mt-4">
{renderWidgetSpecificSettings()}
</TabsContent>
</Tabs>
<div className="mt-6 pt-4 border-t border-border/30">
<Button
variant="outline"
size="sm"
className="w-full"
onClick={resetToDefaults}
>
Reset til Standard
</Button>
</div>
</div>
)}
{/* Widget Content */}
<div className={cn("p-4 sm:p-6", config.expanded && "p-6 sm:p-8")}>
{typeof children === 'function' ? children(config, settings) : children}
</div>
{/* Corner accents */}
<div className={cn("absolute top-0 left-0 w-4 h-4 border-l-2 border-t-2", accentColorClass.split(' ')[1])} />
<div className={cn("absolute top-0 right-0 w-4 h-4 border-r-2 border-t-2", accentColorClass.split(' ')[1])} />
<div className={cn("absolute bottom-0 left-0 w-4 h-4 border-l-2 border-b-2", accentColorClass.split(' ')[1])} />
<div className={cn("absolute bottom-0 right-0 w-4 h-4 border-r-2 border-b-2", accentColorClass.split(' ')[1])} />
</div>
);
};
export default ConfigurableWidget;