From 7dc0fd99ecd75847d8e1c3160b3580bdc6769722 Mon Sep 17 00:00:00 2001 From: Brian Keating Date: Wed, 4 Feb 2026 00:27:49 +0000 Subject: [PATCH] Fix map not visible when both sidebars are hidden - Add dynamic grid columns that adjust based on which sidebars are visible - Add panel visibility settings in Settings panel to toggle individual panels - Add panels config to default config and config loader - Add invalidateSize() call to map when panels toggle (monolithic version) - Add Playwright as dev dependency for testing --- package.json | 3 +- public/index-monolithic.html | 89 ++++- src/App.jsx | 557 +++++++++++++++++-------------- src/components/SettingsPanel.jsx | 211 +++++++++++- src/utils/config.js | 19 +- 5 files changed, 609 insertions(+), 270 deletions(-) diff --git a/package.json b/package.json index ff37c72..0910ef6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "ws": "^8.14.2" }, "devDependencies": { + "@playwright/test": "^1.58.1", "@vitejs/plugin-react": "^4.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -44,4 +45,4 @@ ], "author": "K0CJH", "license": "MIT" -} \ No newline at end of file +} diff --git a/public/index-monolithic.html b/public/index-monolithic.html index 92131ef..4e18ee4 100644 --- a/public/index-monolithic.html +++ b/public/index-monolithic.html @@ -2289,7 +2289,7 @@ // ============================================ // LEAFLET MAP COMPONENT // ============================================ - const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, dxFilters, satellites, showDXPaths, showDXLabels, onToggleDXLabels, showPOTA, showSatellites, onToggleSatellites, hoveredSpot }) => { + const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, dxFilters, satellites, showDXPaths, showDXLabels, onToggleDXLabels, showPOTA, showSatellites, onToggleSatellites, hoveredSpot, showLeftPanel, showRightPanel }) => { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const tileLayerRef = useRef(null); @@ -2420,6 +2420,16 @@ } }, [mapStyle]); + useEffect(() => { + if (!mapInstanceRef.current) return; + const timer = setTimeout(() => { + if (mapInstanceRef.current) { + mapInstanceRef.current.invalidateSize(); + } + }, 100); + return () => clearTimeout(timer); + }, [showLeftPanel, showRightPanel]); + // Update markers and path useEffect(() => { if (!mapInstanceRef.current) return; @@ -5072,7 +5082,34 @@ 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 [showLeftPanel, setShowLeftPanel] = useState(() => { + try { + const stored = localStorage.getItem('openhamclock_showLeftPanel'); + return stored === 'true'; + } catch (e) { return true; } + }); + const [showRightPanel, setShowRightPanel] = useState(() => { + try { + const stored = localStorage.getItem('openhamclock_showRightPanel'); + return stored === 'true'; + } catch (e) { return true; } + }); + + useEffect(() => { + try { + localStorage.setItem('openhamclock_showLeftPanel', showLeftPanel.toString()); + } catch (e) { console.error('Failed to save left panel visibility:', e); } + }, [showLeftPanel]); + useEffect(() => { + try { + localStorage.setItem('openhamclock_showRightPanel', showRightPanel.toString()); + } catch (e) { console.error('Failed to save right panel visibility:', e); } + }, [showRightPanel]); + + const toggleLeftPanel = useCallback(() => setShowLeftPanel(prev => !prev), []); + const toggleRightPanel = useCallback(() => setShowRightPanel(prev => !prev), []); + // 12/24 hour format preference with localStorage persistence const [use12Hour, setUse12Hour] = useState(() => { try { @@ -5298,7 +5335,7 @@ transform: `scale(${scale})`, transformOrigin: 'center center', display: 'grid', - gridTemplateColumns: '280px 1fr 280px', + gridTemplateColumns: `${showLeftPanel ? '280px' : '0'} 1fr ${showRightPanel ? '280px' : '0'}`, gridTemplateRows: '50px 1fr', gap: '8px', padding: '8px', @@ -5359,9 +5396,35 @@
K = 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '600' }}>{spaceWeather.data?.kIndex ?? '--'}
SSN {spaceWeather.data?.sunspotNumber || '--'}
- + {/* Settings & Fullscreen Buttons */}
+ + {/* LEFT SIDEBAR */} -
+
{/* DE Location */}
📍 DE - YOUR LOCATION
@@ -5441,12 +5504,12 @@ {/* CENTER - MAP */}
-
Click map to set DX • 73 de {config.callsign} @@ -5464,7 +5529,7 @@
{/* RIGHT SIDEBAR */} -
+
{/* DX Cluster - Compact with filters */}
diff --git a/src/App.jsx b/src/App.jsx index 39f1c63..73fbf66 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -278,6 +278,33 @@ const App = () => { const localTime = currentTime.toLocaleTimeString('en-US', localTimeOpts); const localDate = currentTime.toLocaleDateString('en-US', localDateOpts); + // Calculate sidebar visibility for responsive grid + const leftSidebarVisible = config.panels?.deLocation?.visible !== false || + config.panels?.dxLocation?.visible !== false || + config.panels?.solar?.visible !== false || + config.panels?.propagation?.visible !== false; + const rightSidebarVisible = config.panels?.dxCluster?.visible !== false || + config.panels?.pskReporter?.visible !== false || + config.panels?.dxpeditions?.visible !== false || + config.panels?.pota?.visible !== false || + config.panels?.contests?.visible !== false; + const leftSidebarWidth = leftSidebarVisible ? '270px' : '0px'; + const rightSidebarWidth = rightSidebarVisible ? '300px' : '0px'; + + // Dynamic grid columns - adjust based on which sidebars are visible + const getGridTemplateColumns = () => { + if (!leftSidebarVisible && !rightSidebarVisible) { + return '1fr'; // Only map visible - single column + } + if (!leftSidebarVisible) { + return `1fr ${rightSidebarWidth}`; // Only right sidebar + } + if (!rightSidebarVisible) { + return `${leftSidebarWidth} 1fr`; // Only left sidebar + } + return `${leftSidebarWidth} 1fr ${rightSidebarWidth}`; // Both sidebars + }; + // Scale for small screens const [scale, setScale] = useState(1); useEffect(() => { @@ -580,10 +607,10 @@ const App = () => { transform: `scale(${scale})`, transformOrigin: 'center center', display: 'grid', - gridTemplateColumns: '270px 1fr 300px', + gridTemplateColumns: getGridTemplateColumns(), gridTemplateRows: '55px 1fr', - gap: '8px', - padding: '8px', + gap: leftSidebarVisible || rightSidebarVisible ? '8px' : '0', + padding: leftSidebarVisible || rightSidebarVisible ? '8px' : '0', overflow: 'hidden', boxSizing: 'border-box' }}> @@ -604,217 +631,227 @@ const App = () => { /> {/* 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 */} - + {leftSidebarVisible && ( +
+ {/* DE Location + Weather */} + {config.panels?.deLocation?.visible !== false && ( +
+
📍 DE - YOUR LOCATION
+
+
{deGrid}
+
{config.location.lat.toFixed(4)}°, {config.location.lon.toFixed(4)}°
+
+ + {deSunTimes.sunrise} + + {deSunTimes.sunset}
- - {/* Expanded details */} - {weatherExpanded && ( -
- {/* Feels like + hi/lo */} -
- {w.feelsLike !== w.temp && ( - Feels like {w.feelsLike}{deg} - )} - {w.todayHigh != null && ( - - ▲{w.todayHigh}° - {' '} - ▼{w.todayLow}° - - )} +
+ + {/* 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} + +
- - {/* Detail grid */} -
-
- 💨 Wind - {w.windDir} {w.windSpeed} {wind} -
-
- 💧 Humidity - {w.humidity}% + {/* 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}° + + )}
- {w.windGusts > w.windSpeed + 5 && ( + + {/* Detail grid */} +
- 🌬️ Gusts - {w.windGusts} {wind} + 💨 Wind + {w.windDir} {w.windSpeed} {wind}
- )} -
- 🌡️ Dew Pt - {w.dewPoint}{deg} -
- {w.pressure && (
- 🔵 Pressure - {w.pressure} hPa + 💧 Humidity + {w.humidity}%
- )} -
- ☁️ Clouds - {w.cloudCover}% -
- {w.visibility && ( + {w.windGusts > w.windSpeed + 5 && ( +
+ 🌬️ Gusts + {w.windGusts} {wind} +
+ )}
- 👁️ Vis - {w.visibility} {vis} + 🌡️ Dew Pt + {w.dewPoint}{deg}
- )} - {w.uvIndex > 0 && ( + {w.pressure && ( +
+ 🔵 Pressure + {w.pressure} hPa +
+ )}
- ☀️ UV - = 8 ? '#ef4444' : w.uvIndex >= 6 ? '#f97316' : w.uvIndex >= 3 ? '#eab308' : 'var(--text-secondary)' }}> - {w.uvIndex.toFixed(1)} - + ☁️ Clouds + {w.cloudCover}%
- )} -
- - {/* 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}% + {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} + {config.panels?.dxLocation?.visible !== false && ( +
+
🎯 DX - TARGET
+
+
{dxGrid}
+
{dxLocation.lat.toFixed(4)}°, {dxLocation.lon.toFixed(4)}°
+
+ + {dxSunTimes.sunrise} + + {dxSunTimes.sunset} +
-
+ )} {/* Solar Panel */} - + {config.panels?.solar?.visible !== false && ( + + )} {/* VOACAP/Propagation Panel */} - + {config.panels?.propagation?.visible !== false && ( + + )}
+ )} {/* CENTER - MAP */} -
+
{
{/* 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 */} -
- + {rightSidebarVisible && ( +
+ {/* DX Cluster - primary panel, takes most space */} + {config.panels?.dxCluster?.visible !== false && ( +
+ setShowDXFilters(true)} + onHoverSpot={setHoveredSpot} + hoveredSpot={hoveredSpot} + showOnMap={mapLayers.showDXPaths} + onToggleMap={toggleDXPaths} + /> +
+ )} + + {/* PSKReporter + WSJT-X - digital mode spots */} + {config.panels?.pskReporter?.visible !== false && ( +
+ 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 */} + {config.panels?.dxpeditions?.visible !== false && ( +
+ +
+ )} + + {/* POTA */} + {config.panels?.pota?.visible !== false && ( +
+ +
+ )} + + {/* Contests - at bottom, compact */} + {config.panels?.contests?.visible !== false && ( +
+ +
+ )}
-
+ )}
)} diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 54bb296..37b0d80 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -23,6 +23,19 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { const [layers, setLayers] = useState([]); const [activeTab, setActiveTab] = useState('station'); + // Panel settings + const [panels, setPanels] = useState(config?.panels || { + deLocation: { visible: true, size: 1.0 }, + dxLocation: { visible: true, size: 1.0 }, + solar: { visible: true, size: 1.0 }, + propagation: { visible: true, size: 1.0 }, + dxCluster: { visible: true, size: 2.0 }, + pskReporter: { visible: true, size: 1.0 }, + dxpeditions: { visible: true, size: 1.0 }, + pota: { visible: true, size: 1.0 }, + contests: { visible: true, size: 1.0 } + }); + useEffect(() => { if (config) { setCallsign(config.callsign || ''); @@ -33,6 +46,17 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { setLayout(config.layout || 'modern'); setTimezone(config.timezone || ''); setDxClusterSource(config.dxClusterSource || 'dxspider-proxy'); + setPanels(config.panels || { + deLocation: { visible: true, size: 1.0 }, + dxLocation: { visible: true, size: 1.0 }, + solar: { visible: true, size: 1.0 }, + propagation: { visible: true, size: 1.0 }, + dxCluster: { visible: true, size: 2.0 }, + pskReporter: { visible: true, size: 1.0 }, + dxpeditions: { visible: true, size: 1.0 }, + pota: { visible: true, size: 1.0 }, + contests: { visible: true, size: 1.0 } + }); if (config.location?.lat && config.location?.lon) { setGridSquare(calculateGridSquare(config.location.lat, config.location.lon)); } @@ -145,6 +169,26 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { } }; + const handleTogglePanel = (panelId) => { + setPanels(prev => ({ + ...prev, + [panelId]: { + ...prev[panelId], + visible: !prev[panelId].visible + } + })); + }; + + const handlePanelSizeChange = (panelId, size) => { + setPanels(prev => ({ + ...prev, + [panelId]: { + ...prev[panelId], + size: parseFloat(size) + } + })); + }; + const handleSave = () => { onSave({ ...config, @@ -154,7 +198,8 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { theme, layout, timezone, - dxClusterSource + dxClusterSource, + panels }); onClose(); }; @@ -237,6 +282,23 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { > ⌇ Station +