diff --git a/index.html b/index.html index 6fda73b..6b1f80c 100644 --- a/index.html +++ b/index.html @@ -207,11 +207,11 @@ const { useState, useEffect, useCallback, useMemo, useRef } = React; // ============================================ - // CONFIGURATION + // CONFIGURATION WITH LOCALSTORAGE // ============================================ - const CONFIG = { - callsign: 'K0CJH', - location: { lat: 39.7392, lon: -104.9903 }, // Denver, CO + const DEFAULT_CONFIG = { + callsign: 'N0CALL', + location: { lat: 40.0150, lon: -105.2705 }, // Boulder, CO (default) defaultDX: { lat: 35.6762, lon: 139.6503 }, // Tokyo refreshIntervals: { spaceWeather: 300000, @@ -222,6 +222,29 @@ } }; + // Load config from localStorage or use defaults + const loadConfig = () => { + try { + const saved = localStorage.getItem('openhamclock_config'); + if (saved) { + const parsed = JSON.parse(saved); + return { ...DEFAULT_CONFIG, ...parsed }; + } + } catch (e) { + console.error('Error loading config:', e); + } + return DEFAULT_CONFIG; + }; + + // Save config to localStorage + const saveConfig = (config) => { + try { + localStorage.setItem('openhamclock_config', JSON.stringify(config)); + } catch (e) { + console.error('Error saving config:', e); + } + }; + // ============================================ // MAP TILE PROVIDERS // ============================================ @@ -403,7 +426,7 @@ } }; fetchData(); - const interval = setInterval(fetchData, CONFIG.refreshIntervals.spaceWeather); + const interval = setInterval(fetchData, DEFAULT_CONFIG.refreshIntervals.spaceWeather); return () => clearInterval(interval); }, []); @@ -443,7 +466,7 @@ finally { setLoading(false); } }; fetchPOTA(); - const interval = setInterval(fetchPOTA, CONFIG.refreshIntervals.pota); + const interval = setInterval(fetchPOTA, DEFAULT_CONFIG.refreshIntervals.pota); return () => clearInterval(interval); }, []); @@ -457,19 +480,51 @@ useEffect(() => { const fetchDX = async () => { try { - // Fallback to sample data since DXWatch may have CORS issues - setData([ - { freq: '14.074', call: 'JA1ABC', comment: 'FT8 -12dB', time: new Date().toISOString().substr(11,5)+'z' }, - { freq: '21.074', call: 'VK2DEF', comment: 'FT8 -08dB', time: new Date().toISOString().substr(11,5)+'z' }, - { freq: '7.040', call: 'DL1XYZ', comment: 'CW 599', time: new Date().toISOString().substr(11,5)+'z' }, - { freq: '14.200', call: 'ZL3QRS', comment: 'SSB 59', time: new Date().toISOString().substr(11,5)+'z' }, - { freq: '28.074', call: 'LU5TUV', comment: 'FT8 -15dB', time: new Date().toISOString().substr(11,5)+'z' } - ]); - } catch (err) { console.error('DX error:', err); } + // Use our proxy endpoint (works when running via server.js) + const response = await fetch('/api/dxcluster/spots'); + + if (response.ok) { + const spots = await response.json(); + if (spots && spots.length > 0) { + setData(spots.slice(0, 15).map(s => ({ + freq: s.freq || (s.frequency ? (parseFloat(s.frequency) / 1000).toFixed(3) : '0.000'), + call: s.call || s.dx_call || 'UNKNOWN', + comment: s.comment || s.info || '', + time: s.time || new Date().toISOString().substr(11, 5) + 'z', + spotter: s.spotter || '' + }))); + } else { + setData([{ + freq: '---', + call: 'NO SPOTS', + comment: 'No DX spots available', + time: '--:--z', + spotter: '' + }]); + } + } else { + setData([{ + freq: '---', + call: 'OFFLINE', + comment: 'DX cluster unavailable', + time: '--:--z', + spotter: '' + }]); + } + } catch (err) { + console.error('DX Cluster error:', err); + setData([{ + freq: '---', + call: 'ERROR', + comment: 'Could not connect to DX cluster', + time: '--:--z', + spotter: '' + }]); + } finally { setLoading(false); } }; fetchDX(); - const interval = setInterval(fetchDX, CONFIG.refreshIntervals.dxCluster); + const interval = setInterval(fetchDX, 30000); // 30 second refresh return () => clearInterval(interval); }, []); @@ -661,7 +716,7 @@ // ============================================ // UI COMPONENTS // ============================================ - const Header = ({ callsign, uptime, version }) => ( + const Header = ({ callsign, uptime, version, onSettingsClick }) => (
OpenHamClock -
{callsign}
+
{callsign}
UPTIME: {uptime} v{version} +
@@ -847,24 +920,288 @@ ); + // ============================================ + // SETTINGS PANEL COMPONENT + // ============================================ + const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { + const [callsign, setCallsign] = useState(config.callsign); + const [lat, setLat] = useState(config.location.lat.toString()); + const [lon, setLon] = useState(config.location.lon.toString()); + const [gridSquare, setGridSquare] = useState(''); + const [useGeolocation, setUseGeolocation] = useState(false); + + // Calculate grid square from lat/lon + useEffect(() => { + const latNum = parseFloat(lat); + const lonNum = parseFloat(lon); + if (!isNaN(latNum) && !isNaN(lonNum)) { + setGridSquare(calculateGridSquare(latNum, lonNum)); + } + }, [lat, lon]); + + const handleGeolocation = () => { + if (navigator.geolocation) { + setUseGeolocation(true); + navigator.geolocation.getCurrentPosition( + (position) => { + setLat(position.coords.latitude.toFixed(6)); + setLon(position.coords.longitude.toFixed(6)); + setUseGeolocation(false); + }, + (error) => { + alert('Could not get location: ' + error.message); + setUseGeolocation(false); + } + ); + } else { + alert('Geolocation is not supported by this browser'); + } + }; + + const handleSave = () => { + const latNum = parseFloat(lat); + const lonNum = parseFloat(lon); + + if (!callsign.trim()) { + alert('Please enter a callsign'); + return; + } + if (isNaN(latNum) || latNum < -90 || latNum > 90) { + alert('Please enter a valid latitude (-90 to 90)'); + return; + } + if (isNaN(lonNum) || lonNum < -180 || lonNum > 180) { + alert('Please enter a valid longitude (-180 to 180)'); + return; + } + + const newConfig = { + ...config, + callsign: callsign.toUpperCase().trim(), + location: { lat: latNum, lon: lonNum } + }; + + onSave(newConfig); + onClose(); + }; + + const handleGridSquareChange = (gs) => { + setGridSquare(gs.toUpperCase()); + // Convert grid square to lat/lon if valid (4 or 6 char) + if (gs.length >= 4) { + try { + const gsUpper = gs.toUpperCase(); + const lonField = gsUpper.charCodeAt(0) - 65; + const latField = gsUpper.charCodeAt(1) - 65; + const lonSquare = parseInt(gsUpper[2]); + const latSquare = parseInt(gsUpper[3]); + + let lonVal = (lonField * 20) + (lonSquare * 2) - 180 + 1; + let latVal = (latField * 10) + latSquare - 90 + 0.5; + + if (gs.length >= 6) { + const lonSub = gsUpper.charCodeAt(4) - 65; + const latSub = gsUpper.charCodeAt(5) - 65; + lonVal = (lonField * 20) + (lonSquare * 2) + (lonSub / 12) - 180 + (1/24); + latVal = (latField * 10) + latSquare + (latSub / 24) - 90 + (1/48); + } + + if (!isNaN(lonVal) && !isNaN(latVal)) { + setLat(latVal.toFixed(6)); + setLon(lonVal.toFixed(6)); + } + } catch (e) { + // Invalid grid square, ignore + } + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +

+ ⚙ Station Settings +

+ + {/* Callsign */} +
+ + setCallsign(e.target.value.toUpperCase())} + placeholder="W1ABC" + style={{ + width: '100%', padding: '12px 16px', background: 'var(--bg-tertiary)', + border: '1px solid var(--border-color)', borderRadius: '6px', + color: 'var(--accent-green)', fontFamily: 'JetBrains Mono, monospace', + fontSize: '18px', outline: 'none', textTransform: 'uppercase' + }} + /> +
+ + {/* Grid Square */} +
+ + handleGridSquareChange(e.target.value)} + placeholder="FN31pr" + maxLength={6} + style={{ + width: '100%', padding: '12px 16px', background: 'var(--bg-tertiary)', + border: '1px solid var(--border-color)', borderRadius: '6px', + color: 'var(--accent-cyan)', fontFamily: 'JetBrains Mono, monospace', + fontSize: '18px', outline: 'none', textTransform: 'uppercase' + }} + /> +
+ + {/* Lat/Lon */} +
+
+ + setLat(e.target.value)} + placeholder="40.0150" + style={{ + width: '100%', padding: '10px 12px', background: 'var(--bg-tertiary)', + border: '1px solid var(--border-color)', borderRadius: '6px', + color: 'var(--text-primary)', fontFamily: 'JetBrains Mono, monospace', + fontSize: '14px', outline: 'none' + }} + /> +
+
+ + setLon(e.target.value)} + placeholder="-105.2705" + style={{ + width: '100%', padding: '10px 12px', background: 'var(--bg-tertiary)', + border: '1px solid var(--border-color)', borderRadius: '6px', + color: 'var(--text-primary)', fontFamily: 'JetBrains Mono, monospace', + fontSize: '14px', outline: 'none' + }} + /> +
+
+ + {/* Get Location Button */} + + + {/* Buttons */} +
+ + +
+ +

+ Settings are saved in your browser +

+
+
+ ); + }; + // ============================================ // MAIN APP // ============================================ const App = () => { + const [config, setConfig] = useState(loadConfig); const [currentTime, setCurrentTime] = useState(new Date()); const [startTime] = useState(Date.now()); const [uptime, setUptime] = useState('0d 0h 0m'); - const [deLocation] = useState(CONFIG.location); - const [dxLocation, setDxLocation] = useState(CONFIG.defaultDX); + const [dxLocation, setDxLocation] = useState(config.defaultDX); + const [showSettings, setShowSettings] = useState(false); + + // Check if this is first run (no config saved) + useEffect(() => { + const saved = localStorage.getItem('openhamclock_config'); + if (!saved) { + // First time user - show settings + setShowSettings(true); + } + }, []); + + const handleSaveConfig = (newConfig) => { + setConfig(newConfig); + saveConfig(newConfig); + }; const spaceWeather = useSpaceWeather(); const bandConditions = useBandConditions(); const potaSpots = usePOTASpots(); const dxCluster = useDXCluster(); - const deGrid = useMemo(() => calculateGridSquare(deLocation.lat, deLocation.lon), [deLocation]); + 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(deLocation.lat, deLocation.lon, currentTime), [deLocation, currentTime]); + 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]); useEffect(() => { @@ -890,23 +1227,23 @@ return (
-
+
setShowSettings(true)} />
{/* Row 1 */} - + {/* Row 2: Map */}
- +
Click anywhere on map to set DX location • Use buttons to change map style
- +
@@ -917,8 +1254,16 @@
- OpenHamClock v3.0.0 | In memory of Elwood Downey WB0OEW | Map tiles © OpenStreetMap, ESRI, CARTO | 73 de {CONFIG.callsign} + OpenHamClock v3.1.0 | In memory of Elwood Downey WB0OEW | Map tiles © OpenStreetMap, ESRI, CARTO | 73 de {config.callsign}
+ + {/* Settings Panel */} + setShowSettings(false)} + config={config} + onSave={handleSaveConfig} + />
); };