From bb16dff9280907736f2751fefe4acf2aff192d7a Mon Sep 17 00:00:00 2001 From: accius Date: Wed, 4 Feb 2026 00:23:27 -0500 Subject: [PATCH] small screen support --- src/App.jsx | 433 ++++++++++++++ src/App.jsx.bak | 947 +++++++++++++++++++++++++++++++ src/components/SettingsPanel.jsx | 8 +- src/lang/de.json | 6 +- src/lang/en.json | 4 + src/lang/es.json | 6 +- src/lang/fr.json | 6 +- src/lang/it.json | 6 +- src/lang/ja.json | 6 +- src/lang/ko.json | 6 +- src/lang/nl.json | 6 +- src/lang/pt.json | 6 +- src/styles/main.css | 102 ++++ 13 files changed, 1531 insertions(+), 11 deletions(-) create mode 100644 src/App.jsx.bak diff --git a/src/App.jsx b/src/App.jsx index b10ab3b..8a5f05e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -572,6 +572,439 @@ const App = () => { 35 + ) : config.layout === 'tablet' ? ( + /* TABLET LAYOUT - Optimized for 7-10" widescreen displays (16:9) */ +
+ {/* COMPACT TOP BAR */} +
+ {/* Callsign */} + setShowSettings(true)} + title="Settings" + > + {config.callsign} + + + {/* UTC */} +
+ UTC + {utcTime} +
+ + {/* Local */} +
+ LOC + {localTime} +
+ + {/* Solar Quick Stats */} +
+ + SFI + {solarIndices?.data?.sfi?.current || spaceWeather?.data?.solarFlux || '--'} + + + K + = 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '700' }}> + {solarIndices?.data?.kp?.current ?? spaceWeather?.data?.kIndex ?? '--'} + + + + SSN + {solarIndices?.data?.ssn?.current || '--'} + +
+ + {/* Controls */} +
+ + +
+
+ + {/* MAIN AREA: Map + Data Sidebar */} +
+ {/* MAP */} +
+ +
+ + {/* DATA SIDEBAR */} +
+ {/* Band Conditions Grid */} +
+
Band Conditions
+
+ {(bandConditions?.data || []).slice(0, 13).map((band, idx) => { + const colors = { + GOOD: { bg: 'rgba(0,255,136,0.2)', color: '#00ff88', border: 'rgba(0,255,136,0.4)' }, + FAIR: { bg: 'rgba(255,180,50,0.2)', color: '#ffb432', border: 'rgba(255,180,50,0.4)' }, + POOR: { bg: 'rgba(255,68,102,0.2)', color: '#ff4466', border: 'rgba(255,68,102,0.4)' } + }; + const s = colors[band.condition] || colors.FAIR; + return ( +
+
{band.band}
+
{band.condition}
+
+ ); + })} +
+ {/* MUF/LUF */} + {propagation.data && ( +
+ MUF {propagation.data.muf || '?'} + LUF {propagation.data.luf || '?'} +
+ )} +
+ + {/* Compact DX Cluster */} +
+
+ DX Cluster + {dxCluster.data?.length || 0} spots +
+
+ {dxCluster.data?.slice(0, 30).map((spot, i) => ( +
setHoveredSpot(spot)} + onMouseLeave={() => setHoveredSpot(null)} + > + {parseFloat(spot.freq).toFixed(1)} + {spot.call} + {spot.time || '--'} +
+ ))} +
+
+
+
+
+ + ) : config.layout === 'compact' ? ( + /* COMPACT LAYOUT - Optimized for 4:3 screens and data-first display */ +
+ {/* TOP: Callsign + Times + Solar */} +
+ {/* Row 1: Callsign + Times */} +
+ setShowSettings(true)} + title="Settings" + > + {config.callsign} + +
+
+
UTC
+
{utcTime}
+
{utcDate}
+
+
+
Local
+
{localTime}
+
{localDate}
+
+
+
+ + +
+
+ {/* Row 2: Solar indices inline */} +
+ + SFI + {solarIndices?.data?.sfi?.current || spaceWeather?.data?.solarFlux || '--'} + + + K + = 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '700' }}> + {solarIndices?.data?.kp?.current ?? spaceWeather?.data?.kIndex ?? '--'} + + + + SSN + {solarIndices?.data?.ssn?.current || '--'} + + {propagation.data && ( + <> + + MUF + {propagation.data.muf || '?'} MHz + + + LUF + {propagation.data.luf || '?'} MHz + + + )} + {localWeather?.data && ( + + {localWeather.data.icon} + {localWeather.data.temp}°{localWeather.data.tempUnit || tempUnit} + + )} +
+
+ + {/* BAND CONDITIONS - Full Width */} +
+
+ {(bandConditions?.data || []).slice(0, 13).map((band, idx) => { + const colors = { + GOOD: { bg: 'rgba(0,255,136,0.2)', color: '#00ff88', border: 'rgba(0,255,136,0.4)' }, + FAIR: { bg: 'rgba(255,180,50,0.2)', color: '#ffb432', border: 'rgba(255,180,50,0.4)' }, + POOR: { bg: 'rgba(255,68,102,0.2)', color: '#ff4466', border: 'rgba(255,68,102,0.4)' } + }; + const s = colors[band.condition] || colors.FAIR; + return ( +
+
{band.band}
+
{band.condition}
+
+ ); + })} +
+
+ + {/* MAIN: Map + DX Cluster side by side */} +
+ {/* Map */} +
+ +
+ {deGrid} → {dxGrid} • Click map to set DX +
+
+ + {/* Compact DX Cluster */} +
+
+ DX Cluster + {dxCluster.data?.length || 0} +
+
+ {dxCluster.data?.slice(0, 40).map((spot, i) => ( +
setHoveredSpot(spot)} + onMouseLeave={() => setHoveredSpot(null)} + > + {parseFloat(spot.freq).toFixed(1)} + {spot.call} + {spot.time || '--'} +
+ ))} +
+
+
+
+ ) : ( /* MODERN LAYOUT */
{ + // Configuration state - initially use defaults, then load from server + const [config, setConfig] = useState(loadConfig); + const [configLoaded, setConfigLoaded] = useState(false); + const [currentTime, setCurrentTime] = useState(new Date()); + const [startTime] = useState(Date.now()); + const [uptime, setUptime] = useState('0d 0h 0m'); + + // Load server configuration on startup (only matters for first-time users) + useEffect(() => { + const initConfig = async () => { + // Fetch server config (provides defaults for new users without localStorage) + await fetchServerConfig(); + + // Load config - localStorage takes priority over server config + const loadedConfig = loadConfig(); + setConfig(loadedConfig); + setConfigLoaded(true); + + // Only show settings if user has no saved config AND no valid callsign + // This prevents the popup from appearing every refresh + const hasLocalStorage = localStorage.getItem('openhamclock_config'); + if (!hasLocalStorage && loadedConfig.callsign === 'N0CALL') { + setShowSettings(true); + } + }; + initConfig(); + }, []); + + // 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 [showPSKFilters, setShowPSKFilters] = useState(false); + const [weatherExpanded, setWeatherExpanded] = useState(() => { + try { return localStorage.getItem('openhamclock_weatherExpanded') === 'true'; } catch { return false; } + }); + const [tempUnit, setTempUnit] = useState(() => { + try { return localStorage.getItem('openhamclock_tempUnit') || 'F'; } catch { return 'F'; } + }); + 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, showPSKReporter: true, showWSJTX: true }; + return stored ? { ...defaults, ...JSON.parse(stored) } : defaults; + } catch (e) { return { showDXPaths: true, showDXLabels: true, showPOTA: true, showSatellites: false, showPSKReporter: true, showWSJTX: true }; } + }); + + 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 })), []); + const togglePSKReporter = useCallback(() => setMapLayers(prev => ({ ...prev, showPSKReporter: !prev.showPSKReporter })), []); + const toggleWSJTX = useCallback(() => setMapLayers(prev => ({ ...prev, showWSJTX: !prev.showWSJTX })), []); + + // 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'); + }, []); + + // Config save handler - persists to localStorage + const handleSaveConfig = (newConfig) => { + setConfig(newConfig); + saveConfig(newConfig); + applyTheme(newConfig.theme || 'dark'); + console.log('[Config] Saved to localStorage:', newConfig.callsign); + }; + + // 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]); + + // PSKReporter Filters + const [pskFilters, setPskFilters] = useState(() => { + try { + const stored = localStorage.getItem('openhamclock_pskFilters'); + return stored ? JSON.parse(stored) : {}; + } catch (e) { return {}; } + }); + + useEffect(() => { + try { + localStorage.setItem('openhamclock_pskFilters', JSON.stringify(pskFilters)); + } catch (e) {} + }, [pskFilters]); + + 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, tempUnit); + const pskReporter = usePSKReporter(config.callsign, { minutes: 15, enabled: config.callsign !== 'N0CALL' }); + const wsjtx = useWSJTX(); + + // Filter PSKReporter spots for map display + const filteredPskSpots = useMemo(() => { + const allSpots = [...(pskReporter.txReports || []), ...(pskReporter.rxReports || [])]; + if (!pskFilters?.bands?.length && !pskFilters?.grids?.length && !pskFilters?.modes?.length) { + return allSpots; + } + return allSpots.filter(spot => { + if (pskFilters?.bands?.length && !pskFilters.bands.includes(spot.band)) return false; + if (pskFilters?.modes?.length && !pskFilters.modes.includes(spot.mode)) return false; + if (pskFilters?.grids?.length) { + const grid = spot.receiverGrid || spot.senderGrid; + if (!grid) return false; + const gridPrefix = grid.substring(0, 2).toUpperCase(); + if (!pskFilters.grids.includes(gridPrefix)) return false; + } + return true; + }); + }, [pskReporter.txReports, pskReporter.rxReports, pskFilters]); + + // Filter WSJT-X decodes for map display (only those with lat/lon from grid) + const wsjtxMapSpots = useMemo(() => { + return wsjtx.decodes.filter(d => d.lat && d.lon && d.type === 'CQ'); + }, [wsjtx.decodes]); + + // 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 — use explicit timezone if configured (fixes privacy browsers like Librewolf + // that spoof timezone to UTC via privacy.resistFingerprinting) + const utcTime = currentTime.toISOString().substr(11, 8); + const utcDate = currentTime.toISOString().substr(0, 10); + const localTimeOpts = { hour12: use12Hour }; + const localDateOpts = { weekday: 'short', month: 'short', day: 'numeric' }; + if (config.timezone) { + localTimeOpts.timeZone = config.timezone; + localDateOpts.timeZone = config.timezone; + } + const localTime = currentTime.toLocaleTimeString('en-US', localTimeOpts); + const localDate = currentTime.toLocaleDateString('en-US', localDateOpts); + + // 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 ( +
+ {config.layout === 'classic' ? ( + /* CLASSIC HAMCLOCK-STYLE LAYOUT */ +
+ {/* TOP BAR - HamClock style */} +
+ {/* Callsign & Time */} +
+
setShowSettings(true)} + title="Click for settings" + > + {config.callsign} +
+
+ Up 35d 18h • v4.20 +
+
+
+ {utcTime}:{String(new Date().getUTCSeconds()).padStart(2, '0')} +
+
+ {utcDate} UTC +
+
+
+ + {/* Solar Indices - SSN & SFI */} +
+ {/* SSN */} +
+
Sunspot Number
+
+
+ {solarIndices?.data?.ssn?.history?.length > 0 && ( + + {(() => { + const data = solarIndices.data.ssn.history.slice(-30); + const values = data.map(d => d.value); + const max = Math.max(...values, 1); + const min = Math.min(...values, 0); + const range = max - min || 1; + const points = data.map((d, i) => { + const x = (i / (data.length - 1)) * 100; + const y = 60 - ((d.value - min) / range) * 55; + return `${x},${y}`; + }).join(' '); + return ; + })()} + + )} +
+
+ {solarIndices?.data?.ssn?.current || '--'} +
+
+
-30 Days
+
+ + {/* SFI */} +
+
10.7 cm Solar flux
+
+
+ {solarIndices?.data?.sfi?.history?.length > 0 && ( + + {(() => { + const data = solarIndices.data.sfi.history.slice(-30); + const values = data.map(d => d.value); + const max = Math.max(...values, 1); + const min = Math.min(...values); + const range = max - min || 1; + const points = data.map((d, i) => { + const x = (i / (data.length - 1)) * 100; + const y = 60 - ((d.value - min) / range) * 55; + return `${x},${y}`; + }).join(' '); + return ; + })()} + + )} +
+
+ {solarIndices?.data?.sfi?.current || '--'} +
+
+
-30 Days +7
+
+
+ + {/* Live Spots & Indices */} +
+ {/* Live Spots by Band */} +
+
Live Spots
+
of {deGrid} - 15 mins
+
+ {[ + { band: '160m', color: '#ff6666' }, + { band: '80m', color: '#ff9966' }, + { band: '60m', color: '#ffcc66' }, + { band: '40m', color: '#ccff66' }, + { band: '30m', color: '#66ff99' }, + { band: '20m', color: '#66ffcc' }, + { band: '17m', color: '#66ccff' }, + { band: '15m', color: '#6699ff' }, + { band: '12m', color: '#9966ff' }, + { band: '10m', color: '#cc66ff' }, + ].map(b => ( +
+ {b.band} + + {dxCluster.data?.filter(s => { + const freq = parseFloat(s.freq); + const bands = { + '160m': [1.8, 2], '80m': [3.5, 4], '60m': [5.3, 5.4], '40m': [7, 7.3], + '30m': [10.1, 10.15], '20m': [14, 14.35], '17m': [18.068, 18.168], + '15m': [21, 21.45], '12m': [24.89, 24.99], '10m': [28, 29.7] + }; + const r = bands[b.band]; + return r && freq >= r[0] && freq <= r[1]; + }).length || 0} + +
+ ))} +
+
+ + {/* Space Weather Indices */} +
+
+
X-Ray
+
M3.0
+
+
+
Kp
+
{solarIndices?.data?.kp?.current ?? spaceWeather?.data?.kIndex ?? '--'}
+
+
+
Bz
+
-0
+
+
+
Aurora
+
18
+
+
+
+
+ + {/* MAIN AREA */} +
+ {/* DX Cluster List */} +
+
+ Cluster + dxspider.co.uk:7300 +
+
+ {dxCluster.data?.slice(0, 25).map((spot, i) => ( +
setHoveredSpot(spot)} + onMouseLeave={() => setHoveredSpot(null)} + > + {parseFloat(spot.freq).toFixed(1)} + {spot.call} + {spot.time || '--'} +
+ ))} +
+
+ + {/* Map */} +
+ + + {/* Settings button overlay */} + +
+
+ + {/* BOTTOM - Frequency Scale */} +
+ MHz + 5 + 10 + 15 + 20 + 25 + 30 + 35 +
+
+ ) : ( + /* MODERN LAYOUT */ +
+ {/* TOP BAR */} +
setShowSettings(true)} + onFullscreenToggle={handleFullscreenToggle} + isFullscreen={isFullscreen} + /> + + {/* LEFT SIDEBAR */} +
+ {/* DE Location + Weather */} +
+
📍 DE - YOUR LOCATION
+
+
{deGrid}
+
{config.location.lat.toFixed(4)}°, {config.location.lon.toFixed(4)}°
+
+ + {deSunTimes.sunrise} + + {deSunTimes.sunset} +
+
+ + {/* Local Weather — compact by default, click to expand */} + {localWeather.data && (() => { + const w = localWeather.data; + const deg = `°${w.tempUnit || tempUnit}`; + const wind = w.windUnit || 'mph'; + const vis = w.visUnit || 'mi'; + return ( +
+ {/* Compact summary row — always visible */} +
+
{ const next = !weatherExpanded; setWeatherExpanded(next); try { localStorage.setItem('openhamclock_weatherExpanded', next.toString()); } catch {} }} + style={{ + display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', + userSelect: 'none', flex: 1, minWidth: 0, + }} + > + {w.icon} + + {w.temp}{deg} + + {w.description} + + 💨{w.windSpeed} + + +
+ {/* F/C toggle */} + +
+ + {/* Expanded details */} + {weatherExpanded && ( +
+ {/* Feels like + hi/lo */} +
+ {w.feelsLike !== w.temp && ( + Feels like {w.feelsLike}{deg} + )} + {w.todayHigh != null && ( + + ▲{w.todayHigh}° + {' '} + ▼{w.todayLow}° + + )} +
+ + {/* Detail grid */} +
+
+ 💨 Wind + {w.windDir} {w.windSpeed} {wind} +
+
+ 💧 Humidity + {w.humidity}% +
+ {w.windGusts > w.windSpeed + 5 && ( +
+ 🌬️ Gusts + {w.windGusts} {wind} +
+ )} +
+ 🌡️ Dew Pt + {w.dewPoint}{deg} +
+ {w.pressure && ( +
+ 🔵 Pressure + {w.pressure} hPa +
+ )} +
+ ☁️ Clouds + {w.cloudCover}% +
+ {w.visibility && ( +
+ 👁️ Vis + {w.visibility} {vis} +
+ )} + {w.uvIndex > 0 && ( +
+ ☀️ UV + = 8 ? '#ef4444' : w.uvIndex >= 6 ? '#f97316' : w.uvIndex >= 3 ? '#eab308' : 'var(--text-secondary)' }}> + {w.uvIndex.toFixed(1)} + +
+ )} +
+ + {/* 3-Day Forecast */} + {w.daily?.length > 0 && ( +
+
FORECAST
+
+ {w.daily.map((day, i) => ( +
+
{i === 0 ? 'Today' : day.date}
+
{day.icon}
+
+ {day.high}° + / + {day.low}° +
+ {day.precipProb > 0 && ( +
+ 💧{day.precipProb}% +
+ )} +
+ ))} +
+
+ )} +
+ )} +
+ ); + })()} +
+ + {/* DX Location */} +
+
🎯 DX - TARGET
+
+
{dxGrid}
+
{dxLocation.lat.toFixed(4)}°, {dxLocation.lon.toFixed(4)}°
+
+ + {dxSunTimes.sunrise} + + {dxSunTimes.sunset} +
+
+
+ + {/* Solar Panel */} + + + {/* VOACAP/Propagation Panel */} + +
+ + {/* CENTER - MAP */} +
+ +
+ Click map to set DX • 73 de {config.callsign} +
+
+ + {/* RIGHT SIDEBAR */} +
+ {/* DX Cluster - primary panel, takes most space */} +
+ setShowDXFilters(true)} + onHoverSpot={setHoveredSpot} + hoveredSpot={hoveredSpot} + showOnMap={mapLayers.showDXPaths} + onToggleMap={toggleDXPaths} + /> +
+ + {/* PSKReporter + WSJT-X - digital mode spots */} +
+ setShowPSKFilters(true)} + onShowOnMap={(report) => { + if (report.lat && report.lon) { + setDxLocation({ lat: report.lat, lon: report.lon, call: report.receiver || report.sender }); + } + }} + wsjtxDecodes={wsjtx.decodes} + wsjtxClients={wsjtx.clients} + wsjtxQsos={wsjtx.qsos} + wsjtxStats={wsjtx.stats} + wsjtxLoading={wsjtx.loading} + wsjtxEnabled={wsjtx.enabled} + wsjtxPort={wsjtx.port} + wsjtxRelayEnabled={wsjtx.relayEnabled} + wsjtxRelayConnected={wsjtx.relayConnected} + wsjtxSessionId={wsjtx.sessionId} + showWSJTXOnMap={mapLayers.showWSJTX} + onToggleWSJTXMap={toggleWSJTX} + /> +
+ + {/* DXpeditions */} +
+ +
+ + {/* POTA */} +
+ +
+ + {/* Contests - at bottom, compact */} +
+ +
+
+
+ )} + + {/* Modals */} + setShowSettings(false)} + config={config} + onSave={handleSaveConfig} + /> + setShowDXFilters(false)} + /> + setShowPSKFilters(false)} + /> +
+ ); +}; + +export default App; diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 54bb296..d10eb61 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -176,7 +176,9 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { const layoutDescriptions = { modern: t('station.settings.layout.modern.describe'), - classic: t('station.settings.layout.classic.describe') + classic: t('station.settings.layout.classic.describe'), + tablet: t('station.settings.layout.tablet.describe'), + compact: t('station.settings.layout.compact.describe') }; return ( @@ -461,7 +463,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { {t('station.settings.layout')}
- {['modern', 'classic'].map((l) => ( + {['modern', 'classic', 'tablet', 'compact'].map((l) => ( ))}
diff --git a/src/lang/de.json b/src/lang/de.json index 7f23a69..bd17759 100644 --- a/src/lang/de.json +++ b/src/lang/de.json @@ -53,5 +53,9 @@ "plugins.layers.earthquakes.description": "Live-USGS-Erdbebendaten (M2,5+ der letzten 24 Stunden)", "plugins.layers.wxradar.name": "Wetterradar", "plugins.layers.wxradar.description": "NEXRAD-Wetterradar-Überlagerung für Nordamerika", - "plugins.layers.wxradar.attribution": "Wetterdaten © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "Wetterdaten © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/lang/en.json b/src/lang/en.json index 49a03bd..08a0d2a 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -28,6 +28,10 @@ "station.settings.layout.classic.describe": "→ Original HamClock-style layout", "station.settings.layout.modern": "Modern", "station.settings.layout.modern.describe": "→ Modern responsive grid layout", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens", "station.settings.latitude": "Latitude", "station.settings.locator": "Grid Square (or enter Lat/Lon below)", "station.settings.longitude": "Longitude", diff --git a/src/lang/es.json b/src/lang/es.json index 4bda44d..dcd99e0 100644 --- a/src/lang/es.json +++ b/src/lang/es.json @@ -54,5 +54,9 @@ "plugins.layers.earthquakes.description": "Datos sísmicos en vivo del USGS (M2.5+ de las últimas 24 horas)", "plugins.layers.wxradar.name": "Radar meteorológico", "plugins.layers.wxradar.description": "Superposición del radar meteorológico NEXRAD para Norteamérica", - "plugins.layers.wxradar.attribution": "Datos meteorológicos © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "Datos meteorológicos © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/lang/fr.json b/src/lang/fr.json index cae97fc..5d33094 100644 --- a/src/lang/fr.json +++ b/src/lang/fr.json @@ -53,5 +53,9 @@ "plugins.layers.earthquakes.description": "Données sismiques USGS en direct (M2,5+ sur les dernières 24 heures)", "plugins.layers.wxradar.name": "Radar météo", "plugins.layers.wxradar.description": "Surcouche du radar météo NEXRAD pour l’Amérique du Nord", - "plugins.layers.wxradar.attribution": "Données météo © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "Données météo © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/lang/it.json b/src/lang/it.json index b18d457..d2d24fa 100644 --- a/src/lang/it.json +++ b/src/lang/it.json @@ -53,5 +53,9 @@ "plugins.layers.earthquakes.description": "Dati sismici USGS in tempo reale (M2,5+ delle ultime 24 ore)", "plugins.layers.wxradar.name": "Radar meteorologico", "plugins.layers.wxradar.description": "Sovrapposizione del radar meteorologico NEXRAD per il Nord America", - "plugins.layers.wxradar.attribution": "Dati meteo © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "Dati meteo © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/lang/ja.json b/src/lang/ja.json index 59d4447..1407a08 100644 --- a/src/lang/ja.json +++ b/src/lang/ja.json @@ -53,5 +53,9 @@ "plugins.layers.earthquakes.description": "USGSのリアルタイム地震データ(過去24時間のM2.5以上)", "plugins.layers.wxradar.name": "気象レーダー", "plugins.layers.wxradar.description": "北米向けNEXRAD気象レーダーのオーバーレイ", - "plugins.layers.wxradar.attribution": "気象データ © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "気象データ © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/lang/ko.json b/src/lang/ko.json index 8d703c0..21db534 100644 --- a/src/lang/ko.json +++ b/src/lang/ko.json @@ -53,5 +53,9 @@ "plugins.layers.earthquakes.description": "USGS 실시간 지진 데이터 (지난 24시간 동안 일어난 규모 M2.5 이상의 지진)", "plugins.layers.wxradar.name": "기상 레이더", "plugins.layers.wxradar.description": "북아메리카 지역 NEXRAD 기상 레이더 오버레이", - "plugins.layers.wxradar.attribution": "기상 데이터 © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "기상 데이터 © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/lang/nl.json b/src/lang/nl.json index b45fcb5..b46b5d9 100644 --- a/src/lang/nl.json +++ b/src/lang/nl.json @@ -53,5 +53,9 @@ "plugins.layers.earthquakes.description": "Live USGS-aardbevingsgegevens (M2,5+ van de afgelopen 24 uur)", "plugins.layers.wxradar.name": "Weerradar", "plugins.layers.wxradar.description": "NEXRAD-weerradaroverlay voor Noord-Amerika", - "plugins.layers.wxradar.attribution": "Weergegevens © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "Weergegevens © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/lang/pt.json b/src/lang/pt.json index 8091927..4cd8cb2 100644 --- a/src/lang/pt.json +++ b/src/lang/pt.json @@ -53,5 +53,9 @@ "plugins.layers.earthquakes.description": "Dados sísmicos do USGS ao vivo (M2,5+ das últimas 24 horas)", "plugins.layers.wxradar.name": "Radar meteorológico", "plugins.layers.wxradar.description": "Sobreposição do radar meteorológico NEXRAD para a América do Norte", - "plugins.layers.wxradar.attribution": "Dados meteorológicos © Iowa State University Mesonet" + "plugins.layers.wxradar.attribution": "Dados meteorológicos © Iowa State University Mesonet", + "station.settings.layout.tablet": "Tablet", + "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)", + "station.settings.layout.compact": "Compact", + "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens" } diff --git a/src/styles/main.css b/src/styles/main.css index ad8b710..eda3aa7 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -905,3 +905,105 @@ body::before { position: relative; z-index: 10000 !important; } + +/* ============================================ + RESPONSIVE: Phone & Small Screen Overrides + ============================================ */ + +/* Tablets and small screens (under 1024px) */ +@media (max-width: 1024px) { + .panel { + padding: 6px !important; + border-radius: 4px !important; + } + + .panel-header { + font-size: 11px !important; + padding: 4px 6px !important; + } +} + +/* Phone landscape and small tablets (under 768px) */ +@media (max-width: 768px) { + /* Force single-column on modern layout */ + .ohc-mobile-stack { + display: flex !important; + flex-direction: column !important; + grid-template-columns: unset !important; + } + + .panel { + padding: 4px !important; + margin-bottom: 2px !important; + border-radius: 3px !important; + } + + .panel-header { + font-size: 10px !important; + padding: 3px 6px !important; + } + + /* Prevent horizontal overflow */ + body { + overflow-x: hidden; + } +} + +/* Phone portrait (under 480px) */ +@media (max-width: 480px) { + /* Extra compact for phones */ + .panel { + padding: 3px !important; + font-size: 11px !important; + } + + .panel-header { + font-size: 9px !important; + } + + /* Reduce font sizes globally for very small screens */ + body { + font-size: 12px; + } +} + +/* Short screens (Pi 7" touchscreen at 480px height) */ +@media (max-height: 520px) { + .panel { + padding: 3px !important; + } + + .panel-header { + padding: 2px 4px !important; + font-size: 10px !important; + } +} + +/* Touch device improvements */ +@media (pointer: coarse) { + /* Larger tap targets for touchscreens */ + button { + min-height: 36px; + min-width: 36px; + } + + /* Prevent text selection on tap */ + .panel-header { + user-select: none; + -webkit-user-select: none; + } + + /* Scrollbar styling for touch */ + ::-webkit-scrollbar { + width: 4px; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 2px; + } +}