Spaces:
Paused
Paused
| 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; | |