/** * SettingsPanel Component * Full settings modal with map layer controls */ import React, { useState, useEffect } from 'react'; import { calculateGridSquare } from '../utils/geo.js'; import { useTranslation, Trans } from 'react-i18next'; import { LANGUAGES } from '../lang/i18n.js'; export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { const [callsign, setCallsign] = useState(config?.callsign || ''); const [callsignSize, setCallsignSize] = useState(config?.callsignSize || 1.0); const [gridSquare, setGridSquare] = useState(''); const [lat, setLat] = useState(config?.location?.lat || 0); const [lon, setLon] = useState(config?.location?.lon || 0); const [theme, setTheme] = useState(config?.theme || 'dark'); const [layout, setLayout] = useState(config?.layout || 'modern'); const [timezone, setTimezone] = useState(config?.timezone || ''); const [dxClusterSource, setDxClusterSource] = useState(config?.dxClusterSource || 'dxspider-proxy'); const { t, i18n } = useTranslation(); // Layer controls const [layers, setLayers] = useState([]); const [activeTab, setActiveTab] = useState('station'); useEffect(() => { if (config) { setCallsign(config.callsign || ''); setCallsignSize(config.callsignSize || 1.0) setLat(config.location?.lat || 0); setLon(config.location?.lon || 0); setTheme(config.theme || 'dark'); setLayout(config.layout || 'modern'); setTimezone(config.timezone || ''); setDxClusterSource(config.dxClusterSource || 'dxspider-proxy'); if (config.location?.lat && config.location?.lon) { setGridSquare(calculateGridSquare(config.location.lat, config.location.lon)); } } }, [config, isOpen]); // Load layers when panel opens useEffect(() => { if (isOpen && window.hamclockLayerControls) { setLayers(window.hamclockLayerControls.layers || []); } }, [isOpen]); // Refresh layers periodically useEffect(() => { if (isOpen && activeTab === 'layers') { const interval = setInterval(() => { if (window.hamclockLayerControls) { setLayers([...window.hamclockLayerControls.layers]); } }, 200); return () => clearInterval(interval); } }, [isOpen, activeTab]); const handleGridChange = (grid) => { setGridSquare(grid.toUpperCase()); if (grid.length >= 4) { const parsed = parseGridSquare(grid); if (parsed) { setLat(parsed.lat); setLon(parsed.lon); } } }; const parseGridSquare = (grid) => { grid = grid.toUpperCase(); if (grid.length < 4) return null; const lon1 = (grid.charCodeAt(0) - 65) * 20 - 180; const lat1 = (grid.charCodeAt(1) - 65) * 10 - 90; const lon2 = parseInt(grid[2]) * 2; const lat2 = parseInt(grid[3]) * 1; let lon = lon1 + lon2 + 1; let lat = lat1 + lat2 + 0.5; if (grid.length >= 6) { const lon3 = (grid.charCodeAt(4) - 65) * (2/24); const lat3 = (grid.charCodeAt(5) - 65) * (1/24); lon = lon1 + lon2 + lon3 + (1/24); lat = lat1 + lat2 + lat3 + (1/48); } return { lat, lon }; }; useEffect(() => { if (lat && lon) { setGridSquare(calculateGridSquare(lat, lon)); } }, [lat, lon]); const handleUseLocation = () => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { setLat(position.coords.latitude); setLon(position.coords.longitude); }, (error) => { console.error('Geolocation error:', error); alert(t('station.settings.useLocation.error1')); } ); } else { alert(t('station.settings.useLocation.error2')); } }; const handleToggleLayer = (layerId) => { if (window.hamclockLayerControls) { const layer = layers.find(l => l.id === layerId); const newEnabledState = !layer.enabled; // Update the control window.hamclockLayerControls.toggleLayer(layerId, newEnabledState); // Force immediate UI update setLayers(prevLayers => prevLayers.map(l => l.id === layerId ? { ...l, enabled: newEnabledState } : l ) ); // Refresh after a short delay to get the updated state setTimeout(() => { if (window.hamclockLayerControls) { setLayers([...window.hamclockLayerControls.layers]); } }, 100); } }; const handleOpacityChange = (layerId, opacity) => { if (window.hamclockLayerControls) { window.hamclockLayerControls.setOpacity(layerId, opacity); setLayers([...window.hamclockLayerControls.layers]); } }; const handleSave = () => { onSave({ ...config, callsign: callsign.toUpperCase(), callsignSize: callsignSize, location: { lat: parseFloat(lat), lon: parseFloat(lon) }, theme, layout, timezone, dxClusterSource }); onClose(); }; if (!isOpen) return null; const Code = ({ children }) => ( {children} ); const themeDescriptions = { dark: t('station.settings.theme.dark.describe'), light: t('station.settings.theme.light.describe'), legacy: t('station.settings.theme.legacy.describe'), retro: t('station.settings.theme.retro.describe') }; const layoutDescriptions = { modern: t('station.settings.layout.modern.describe'), classic: t('station.settings.layout.classic.describe') }; return (

{t('station.settings.title')}

{/* Tab Navigation */}
{/* Station Settings Tab */} {activeTab === 'station' && ( <> {/* First-time setup banner */} {(config?.configIncomplete || config?.callsign === 'N0CALL' || !config?.locator) && (
{t("station.settings.welcome")}
{t("station.settings.describe")}
, env: }} />
)} {/* Callsign */}
setCallsign(e.target.value.toUpperCase())} style={{ width: '100%', padding: '12px', background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: '6px', color: 'var(--accent-amber)', fontSize: '18px', fontFamily: 'JetBrains Mono, monospace', fontWeight: '700', boxSizing: 'border-box' }} />
{/* Callsign Size*/}
{ if (e.target.value >= 0.1 && e.target.value <= 2.0) { setCallsignSize(e.target.value) }}} style={{ width: '100%', padding: '10px', background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: '6px', color: 'var(--text-primary)', fontSize: '14px', fontFamily: 'JetBrains Mono, monospace', boxSizing: 'border-box' }} />
{/* Grid Square */}
handleGridChange(e.target.value)} placeholder="FN20nc" maxLength={6} style={{ width: '100%', padding: '12px', background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: '6px', color: 'var(--accent-amber)', fontSize: '18px', fontFamily: 'JetBrains Mono, monospace', fontWeight: '700', boxSizing: 'border-box' }} />
{/* Lat/Lon */}
setLat(parseFloat(e.target.value) || 0)} style={{ width: '100%', padding: '10px', background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: '6px', color: 'var(--text-primary)', fontSize: '14px', fontFamily: 'JetBrains Mono, monospace', boxSizing: 'border-box' }} />
setLon(parseFloat(e.target.value) || 0)} style={{ width: '100%', padding: '10px', background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: '6px', color: 'var(--text-primary)', fontSize: '14px', fontFamily: 'JetBrains Mono, monospace', boxSizing: 'border-box' }} />
{/* Theme */}
{['dark', 'light', 'legacy', 'retro'].map((th) => ( ))}
{themeDescriptions[theme]}
{/* Layout */}
{['modern', 'classic'].map((l) => ( ))}
{layoutDescriptions[layout]}
{/* DX Cluster Source */}
Set this if your local time shows incorrectly (e.g. same as UTC). Privacy browsers like Librewolf may spoof your timezone. {timezone ? '' : ' Currently using browser default.'}
{/* DX Cluster Source - original */}
{t('station.settings.dx.describe')}
{/* Language */}
{LANGUAGES.map((lang) => ( ))}
)} {/* Map Layers Tab */} {activeTab === 'layers' && (
{layers.length > 0 ? ( layers.map(layer => (
{layer.category}
{layer.enabled && (
handleOpacityChange(layer.id, parseFloat(e.target.value) / 100)} style={{ width: '100%', cursor: 'pointer' }} />
)}
)) ) : (
No map layers available
)}
)} {/* Buttons */}
{t('station.settings.button.save.confirm')}
); }; export default SettingsPanel;