+64 27 7250 007
import React, { useState, useRef, useEffect } from 'react'; import { Play, Pause, Volume2, VolumeX, Radio, RefreshCw, Activity, Wifi, WifiOff } from 'lucide-react'; // Default Configuration (Update with your live stream and WebSocket API endpoints) const DEFAULT_STREAM_URL = "https://stream.zeno.fm/f3wvbb1424ztv"; const DEFAULT_WS_URL = "wss://api.focus360.io/v1/market-stream"; export default function MarketDashboardWidget() { // --- Radio State & Refs --- const audioRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [isMuted, setIsMuted] = useState(false); const [volume, setVolume] = useState(0.8); const [radioStatus, setRadioStatus] = useState('idle'); // 'idle' | 'connecting' | 'playing' | 'error' const [streamUrl, setStreamUrl] = useState(DEFAULT_STREAM_URL); const [currentSegment, setCurrentSegment] = useState('Live Audio Stream'); // --- Real-time Data & WebSocket State --- const [wsUrl, setWsUrl] = useState(DEFAULT_WS_URL); const [wsConnected, setWsConnected] = useState(false); const [dataPoints, setDataPoints] = useState([100]); const wsRef = useRef(null); // --- Chart Canvas Ref --- const canvasRef = useRef(null); // 1. Sync Radio Volume useEffect(() => { if (audioRef.current) { audioRef.current.volume = isMuted ? 0 : volume; } }, [volume, isMuted]); // 2. Real-time WebSocket Client Connection useEffect(() => { let ws; try { ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { setWsConnected(true); // Subscribe to live market data feed ws.send(JSON.stringify({ action: 'subscribe', channel: 'FOCUS360_MARKETS' })); }; ws.onmessage = (event) => { try { const payload = JSON.parse(event.data); // Handle incoming price ticks if (payload.type === 'TICK' && typeof payload.price === 'number') { setDataPoints((prevPoints) => { const updated = [...prevPoints, payload.price]; return updated.slice(-60); // Retain last 60 ticks for graph rendering }); } // Handle incoming radio stream metadata if (payload.type === 'RADIO_META' && payload.title) { setCurrentSegment(payload.title); } } catch (err) { console.error("Error parsing WebSocket frame:", err); } }; ws.onerror = () => setWsConnected(false); ws.onclose = () => setWsConnected(false); } catch (err) { console.warn("WebSocket setup failed, fallback mode active:", err); setWsConnected(false); } return () => { if (ws) ws.close(); }; }, [wsUrl]); // Fallback Simulation when WebSocket is offline useEffect(() => { if (wsConnected) return; const interval = setInterval(() => { setDataPoints((prevPoints) => { const lastVal = prevPoints[prevPoints.length - 1] || 100; const nextVal = lastVal + (Math.random() - 0.48) * 2; const updated = [...prevPoints, nextVal]; return updated.slice(-60); }); }, 500); return () => clearInterval(interval); }, [wsConnected]); // 3. Render Chart from Real-time Data Stream useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; ctx.clearRect(0, 0, width, height); if (dataPoints.length === 0) return; const min = Math.min(...dataPoints) - 2; const max = Math.max(...dataPoints) + 2; // Draw Grid ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1; for (let y = 0; y < height; y += 30) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } // Draw Price Trend ctx.beginPath(); ctx.strokeStyle = radioStatus === 'playing' ? '#10b981' : '#3b82f6'; ctx.lineWidth = 2; dataPoints.forEach((pt, idx) => { const x = (idx / Math.max(dataPoints.length - 1, 1)) * width; const y = height - ((pt - min) / (max - min || 1)) * height; if (idx === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // Fill Gradient Area ctx.lineTo(width, height); ctx.lineTo(0, height); ctx.fillStyle = radioStatus === 'playing' ? 'rgba(16, 185, 129, 0.08)' : 'rgba(59, 130, 246, 0.08)'; ctx.fill(); }, [dataPoints, radioStatus]); // 4. Radio Controls const togglePlay = () => { const audio = audioRef.current; if (!audio) return; if (isPlaying) { audio.pause(); audio.src = ''; // Drop buffer for live stream sync setIsPlaying(false); setRadioStatus('idle'); } else { setRadioStatus('connecting'); audio.src = streamUrl; audio.play() .then(() => { setIsPlaying(true); setRadioStatus('playing'); }) .catch((err) => { console.error("Radio playback error:", err); setRadioStatus('error'); setIsPlaying(false); }); } }; const toggleMute = () => setIsMuted(!isMuted); return (
{/* Radio Audio Element */}
); }