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 @@
+
+ {/* Settings Panel */}
+ setShowSettings(false)}
+ config={config}
+ onSave={handleSaveConfig}
+ />
);
};