Spaces:
Paused
Paused
| import { useState, useEffect } from 'react'; | |
| import { X, ChevronUp, ChevronDown, Settings, Plus, Trash2 } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { | |
| Popover, | |
| PopoverContent, | |
| PopoverTrigger, | |
| } from '@/components/ui/popover'; | |
| import { cn } from '@/lib/utils'; | |
| type TickerPosition = 'top' | 'bottom' | 'hidden'; | |
| const TICKER_SETTINGS_KEY = 'cyber-ticker-settings'; | |
| const CUSTOM_HEADLINES_KEY = 'cyber-ticker-custom-headlines'; | |
| interface TickerSettings { | |
| position: TickerPosition; | |
| opacity: number; | |
| } | |
| const defaultSettings: TickerSettings = { | |
| position: 'top', | |
| opacity: 60, | |
| }; | |
| const defaultHeadlines = [ | |
| "SYSTEM AKTIV", | |
| "ALLE SYSTEMER ONLINE", | |
| "SIKKERHEDSNIVEAU: MAKSIMUM", | |
| "DATASTREAM STABIL", | |
| "NEURAL NETVÆRK OPERATIONELT", | |
| "KRYPTERING: AES-256", | |
| "FORBINDELSE SIKRET", | |
| "PROTOKOL INITIERET", | |
| ]; | |
| const BreakingTicker = () => { | |
| const [settings, setSettings] = useState<TickerSettings>(defaultSettings); | |
| const [customHeadlines, setCustomHeadlines] = useState<string[]>([]); | |
| const [newHeadline, setNewHeadline] = useState(''); | |
| useEffect(() => { | |
| const savedSettings = localStorage.getItem(TICKER_SETTINGS_KEY); | |
| if (savedSettings) { | |
| try { | |
| setSettings(JSON.parse(savedSettings)); | |
| } catch {} | |
| } | |
| const savedHeadlines = localStorage.getItem(CUSTOM_HEADLINES_KEY); | |
| if (savedHeadlines) { | |
| try { | |
| setCustomHeadlines(JSON.parse(savedHeadlines)); | |
| } catch {} | |
| } | |
| }, []); | |
| const updateSettings = (updates: Partial<TickerSettings>) => { | |
| const newSettings = { ...settings, ...updates }; | |
| setSettings(newSettings); | |
| localStorage.setItem(TICKER_SETTINGS_KEY, JSON.stringify(newSettings)); | |
| }; | |
| const addHeadline = () => { | |
| if (newHeadline.trim()) { | |
| const updated = [...customHeadlines, newHeadline.trim().toUpperCase()]; | |
| setCustomHeadlines(updated); | |
| localStorage.setItem(CUSTOM_HEADLINES_KEY, JSON.stringify(updated)); | |
| setNewHeadline(''); | |
| } | |
| }; | |
| const removeHeadline = (index: number) => { | |
| const updated = customHeadlines.filter((_, i) => i !== index); | |
| setCustomHeadlines(updated); | |
| localStorage.setItem(CUSTOM_HEADLINES_KEY, JSON.stringify(updated)); | |
| }; | |
| const allHeadlines = [...customHeadlines, ...defaultHeadlines]; | |
| const repeatedHeadlines = [...allHeadlines, ...allHeadlines, ...allHeadlines]; | |
| if (settings.position === 'hidden') { | |
| return ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="fixed bottom-4 right-4 z-50 bg-destructive/20 hover:bg-destructive/40 text-destructive" | |
| onClick={() => updateSettings({ position: 'top' })} | |
| > | |
| Vis ticker | |
| </Button> | |
| ); | |
| } | |
| const positionClasses = settings.position === 'top' | |
| ? 'top-0' | |
| : 'bottom-0'; | |
| return ( | |
| <div | |
| className={cn( | |
| "fixed left-0 right-0 z-40 backdrop-blur-sm border-destructive/50 pointer-events-auto", | |
| positionClasses, | |
| settings.position === 'top' ? 'border-b' : 'border-t' | |
| )} | |
| style={{ | |
| backgroundColor: `hsl(var(--destructive) / ${settings.opacity / 100})`, | |
| }} | |
| > | |
| <div className="flex items-center"> | |
| <div | |
| className="px-3 py-1.5 font-display text-xs font-bold text-destructive uppercase tracking-wider shrink-0 flex items-center gap-2" | |
| style={{ backgroundColor: `hsl(var(--background) / 0.9)` }} | |
| > | |
| <span className="inline-block animate-pulse text-destructive">●</span> | |
| <span className="text-destructive">BREAKING</span> | |
| </div> | |
| <div className="ticker flex-1 py-1.5 overflow-hidden"> | |
| <div className="ticker-content font-mono text-xs text-destructive-foreground/90"> | |
| {repeatedHeadlines.map((headline, i) => ( | |
| <span key={i} className="mx-6"> | |
| {headline} | |
| <span className="mx-3 text-destructive-foreground/40">///</span> | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-1 px-2 shrink-0"> | |
| <Popover> | |
| <PopoverTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-6 w-6 p-0 text-destructive-foreground/70 hover:text-destructive-foreground hover:bg-destructive/30" | |
| > | |
| <Settings className="w-3 h-3" /> | |
| </Button> | |
| </PopoverTrigger> | |
| <PopoverContent align="end" className="w-72 p-3 bg-card border-border"> | |
| <div className="space-y-4"> | |
| <div className="space-y-2"> | |
| <div className="text-xs font-medium text-muted-foreground">Gennemsigtighed</div> | |
| <input | |
| type="range" | |
| min="20" | |
| max="100" | |
| value={settings.opacity} | |
| onChange={(e) => updateSettings({ opacity: Number(e.target.value) })} | |
| className="w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer" | |
| /> | |
| <div className="text-xs text-muted-foreground text-right">{settings.opacity}%</div> | |
| </div> | |
| <div className="border-t border-border pt-3 space-y-2"> | |
| <div className="text-xs font-medium text-muted-foreground">Custom Headlines</div> | |
| <div className="flex gap-2"> | |
| <Input | |
| value={newHeadline} | |
| onChange={(e) => setNewHeadline(e.target.value)} | |
| placeholder="Tilføj headline..." | |
| className="h-7 text-xs" | |
| onKeyDown={(e) => e.key === 'Enter' && addHeadline()} | |
| /> | |
| <Button size="sm" className="h-7 px-2" onClick={addHeadline}> | |
| <Plus className="w-3 h-3" /> | |
| </Button> | |
| </div> | |
| {customHeadlines.length > 0 && ( | |
| <div className="space-y-1 max-h-32 overflow-y-auto"> | |
| {customHeadlines.map((headline, i) => ( | |
| <div key={i} className="flex items-center justify-between gap-2 text-xs bg-secondary/50 rounded px-2 py-1"> | |
| <span className="truncate font-mono">{headline}</span> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-5 w-5 p-0 text-destructive hover:text-destructive" | |
| onClick={() => removeHeadline(i)} | |
| > | |
| <Trash2 className="w-3 h-3" /> | |
| </Button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </PopoverContent> | |
| </Popover> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-6 w-6 p-0 text-destructive-foreground/70 hover:text-destructive-foreground hover:bg-destructive/30" | |
| onClick={() => updateSettings({ position: settings.position === 'top' ? 'bottom' : 'top' })} | |
| title={settings.position === 'top' ? 'Flyt til bund' : 'Flyt til top'} | |
| > | |
| {settings.position === 'top' ? <ChevronDown className="w-3 h-3" /> : <ChevronUp className="w-3 h-3" />} | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-6 w-6 p-0 text-destructive-foreground/70 hover:text-destructive-foreground hover:bg-destructive/30" | |
| onClick={() => updateSettings({ position: 'hidden' })} | |
| title="Skjul ticker" | |
| > | |
| <X className="w-3 h-3" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default BreakingTicker; | |