/** * OpenHamClock - Main Application Component * Amateur Radio Dashboard v3.7.0 */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; // Components import { Header, WorldMap, DXClusterPanel, POTAPanel, ContestPanel, SettingsPanel, DXFilterManager, SolarPanel, PropagationPanel, DXpeditionPanel } from './components'; // Hooks import { useSpaceWeather, useBandConditions, useDXCluster, useDXPaths, usePOTASpots, useContests, useLocalWeather, usePropagation, useMySpots, useDXpeditions, useSatellites, useSolarIndices } from './hooks'; // Utils import { loadConfig, saveConfig, applyTheme, calculateGridSquare, calculateSunTimes } from './utils'; const App = () => { // Configuration state const [config, setConfig] = useState(loadConfig); const [currentTime, setCurrentTime] = useState(new Date()); const [startTime] = useState(Date.now()); const [uptime, setUptime] = useState('0d 0h 0m'); // DX Location with localStorage persistence const [dxLocation, setDxLocation] = useState(() => { try { const stored = localStorage.getItem('openhamclock_dxLocation'); if (stored) { const parsed = JSON.parse(stored); if (parsed.lat && parsed.lon) return parsed; } } catch (e) {} return config.defaultDX; }); useEffect(() => { try { localStorage.setItem('openhamclock_dxLocation', JSON.stringify(dxLocation)); } catch (e) {} }, [dxLocation]); // UI state const [showSettings, setShowSettings] = useState(false); const [showDXFilters, setShowDXFilters] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); // Map layer visibility const [mapLayers, setMapLayers] = useState(() => { try { const stored = localStorage.getItem('openhamclock_mapLayers'); const defaults = { showDXPaths: true, showDXLabels: true, showPOTA: true, showSatellites: false }; return stored ? { ...defaults, ...JSON.parse(stored) } : defaults; } catch (e) { return { showDXPaths: true, showDXLabels: true, showPOTA: true, showSatellites: false }; } }); useEffect(() => { try { localStorage.setItem('openhamclock_mapLayers', JSON.stringify(mapLayers)); } catch (e) {} }, [mapLayers]); const [hoveredSpot, setHoveredSpot] = useState(null); const toggleDXPaths = useCallback(() => setMapLayers(prev => ({ ...prev, showDXPaths: !prev.showDXPaths })), []); const toggleDXLabels = useCallback(() => setMapLayers(prev => ({ ...prev, showDXLabels: !prev.showDXLabels })), []); const togglePOTA = useCallback(() => setMapLayers(prev => ({ ...prev, showPOTA: !prev.showPOTA })), []); const toggleSatellites = useCallback(() => setMapLayers(prev => ({ ...prev, showSatellites: !prev.showSatellites })), []); // 12/24 hour format const [use12Hour, setUse12Hour] = useState(() => { try { return localStorage.getItem('openhamclock_use12Hour') === 'true'; } catch (e) { return false; } }); useEffect(() => { try { localStorage.setItem('openhamclock_use12Hour', use12Hour.toString()); } catch (e) {} }, [use12Hour]); const handleTimeFormatToggle = useCallback(() => setUse12Hour(prev => !prev), []); // Fullscreen const handleFullscreenToggle = useCallback(() => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().then(() => setIsFullscreen(true)).catch(() => {}); } else { document.exitFullscreen().then(() => setIsFullscreen(false)).catch(() => {}); } }, []); useEffect(() => { const handler = () => setIsFullscreen(!!document.fullscreenElement); document.addEventListener('fullscreenchange', handler); return () => document.removeEventListener('fullscreenchange', handler); }, []); useEffect(() => { applyTheme(config.theme || 'dark'); }, []); useEffect(() => { const saved = localStorage.getItem('openhamclock_config'); if (!saved) setShowSettings(true); }, []); const handleSaveConfig = (newConfig) => { setConfig(newConfig); saveConfig(newConfig); applyTheme(newConfig.theme || 'dark'); }; // Data hooks const spaceWeather = useSpaceWeather(); const bandConditions = useBandConditions(spaceWeather.data); const solarIndices = useSolarIndices(); const potaSpots = usePOTASpots(); // DX Filters const [dxFilters, setDxFilters] = useState(() => { try { const stored = localStorage.getItem('openhamclock_dxFilters'); return stored ? JSON.parse(stored) : {}; } catch (e) { return {}; } }); useEffect(() => { try { localStorage.setItem('openhamclock_dxFilters', JSON.stringify(dxFilters)); } catch (e) {} }, [dxFilters]); const dxCluster = useDXCluster(config.dxClusterSource || 'auto', dxFilters); const dxPaths = useDXPaths(); const dxpeditions = useDXpeditions(); const contests = useContests(); const propagation = usePropagation(config.location, dxLocation); const mySpots = useMySpots(config.callsign); const satellites = useSatellites(config.location); const localWeather = useLocalWeather(config.location); // Computed values const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]); const dxGrid = useMemo(() => calculateGridSquare(dxLocation.lat, dxLocation.lon), [dxLocation]); const deSunTimes = useMemo(() => calculateSunTimes(config.location.lat, config.location.lon, currentTime), [config.location, currentTime]); const dxSunTimes = useMemo(() => calculateSunTimes(dxLocation.lat, dxLocation.lon, currentTime), [dxLocation, currentTime]); // Time update useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()); const elapsed = Date.now() - startTime; const d = Math.floor(elapsed / 86400000); const h = Math.floor((elapsed % 86400000) / 3600000); const m = Math.floor((elapsed % 3600000) / 60000); setUptime(`${d}d ${h}h ${m}m`); }, 1000); return () => clearInterval(timer); }, [startTime]); const handleDXChange = useCallback((coords) => { setDxLocation({ lat: coords.lat, lon: coords.lon }); }, []); // Format times const utcTime = currentTime.toISOString().substr(11, 8); const localTime = currentTime.toLocaleTimeString('en-US', { hour12: use12Hour }); const utcDate = currentTime.toISOString().substr(0, 10); const localDate = currentTime.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); // Scale for small screens const [scale, setScale] = useState(1); useEffect(() => { const calculateScale = () => { const minWidth = 1200; const minHeight = 800; const scaleX = window.innerWidth / minWidth; const scaleY = window.innerHeight / minHeight; setScale(Math.min(scaleX, scaleY, 1)); }; calculateScale(); window.addEventListener('resize', calculateScale); return () => window.removeEventListener('resize', calculateScale); }, []); return (
{/* TOP BAR */}
setShowSettings(true)} onFullscreenToggle={handleFullscreenToggle} isFullscreen={isFullscreen} /> {/* LEFT SIDEBAR */}
{/* DE Location */}
📍 DE - YOUR LOCATION
{deGrid}
{config.location.lat.toFixed(2)}°, {config.location.lon.toFixed(2)}°
{deSunTimes.sunrise} {deSunTimes.sunset}
{/* DX Location */}
🎯 DX - TARGET
{dxGrid}
{dxLocation.lat.toFixed(2)}°, {dxLocation.lon.toFixed(2)}°
{dxSunTimes.sunrise} {dxSunTimes.sunset}
{/* Solar Panel */} {/* VOACAP/Propagation Panel */}
{/* CENTER - MAP */}
Click map to set DX • 73 de {config.callsign}
{/* RIGHT SIDEBAR */}
{/* DX Cluster - takes most space */}
setShowDXFilters(true)} onHoverSpot={setHoveredSpot} hoveredSpot={hoveredSpot} showOnMap={mapLayers.showDXPaths} onToggleMap={toggleDXPaths} />
{/* DXpeditions - smaller */}
{/* POTA - smaller */}
{/* Contests - smaller */}
{/* Modals */} setShowSettings(false)} config={config} onSave={handleSaveConfig} /> setShowDXFilters(false)} />
); }; export default App;