More N3DD Requests

Local weather and 12/24 time
pull/27/head
accius 4 days ago
parent 5cdcd7a648
commit 9987442c7c

@ -285,6 +285,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
**73 de K0CJH and the OpenHamClock contributors!**
*The original HamClock will cease to function in June 2026. OpenHamClock carries forward Elwood's legacy with modern technology and open-source community development.*
*"The original HamClock will cease to function in June 2026. OpenHamClock carries forward Elwood's legacy with modern technology and open-source community development."*
</div>

@ -813,6 +813,86 @@
return { data, loading };
};
// ============================================
// LOCAL WEATHER HOOK - Using Open-Meteo API (free, no API key)
// ============================================
const useLocalWeather = (location) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!location || !location.lat || !location.lon) {
setLoading(false);
return;
}
const fetchWeather = async () => {
try {
// Open-Meteo free API - no key required
const url = `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=fahrenheit&wind_speed_unit=mph&timezone=auto`;
const response = await fetch(url);
if (response.ok) {
const result = await response.json();
const current = result.current;
// Weather code to description mapping
const weatherCodes = {
0: { desc: 'Clear', icon: '☀️' },
1: { desc: 'Mostly Clear', icon: '🌤️' },
2: { desc: 'Partly Cloudy', icon: '⛅' },
3: { desc: 'Overcast', icon: '☁️' },
45: { desc: 'Foggy', icon: '🌫️' },
48: { desc: 'Rime Fog', icon: '🌫️' },
51: { desc: 'Light Drizzle', icon: '🌧️' },
53: { desc: 'Drizzle', icon: '🌧️' },
55: { desc: 'Heavy Drizzle', icon: '🌧️' },
61: { desc: 'Light Rain', icon: '🌧️' },
63: { desc: 'Rain', icon: '🌧️' },
65: { desc: 'Heavy Rain', icon: '🌧️' },
71: { desc: 'Light Snow', icon: '🌨️' },
73: { desc: 'Snow', icon: '🌨️' },
75: { desc: 'Heavy Snow', icon: '🌨️' },
77: { desc: 'Snow Grains', icon: '🌨️' },
80: { desc: 'Light Showers', icon: '🌦️' },
81: { desc: 'Showers', icon: '🌦️' },
82: { desc: 'Heavy Showers', icon: '🌦️' },
85: { desc: 'Snow Showers', icon: '🌨️' },
86: { desc: 'Heavy Snow Showers', icon: '🌨️' },
95: { desc: 'Thunderstorm', icon: '⛈️' },
96: { desc: 'Thunderstorm w/ Hail', icon: '⛈️' },
99: { desc: 'Severe Thunderstorm', icon: '⛈️' }
};
const weather = weatherCodes[current.weather_code] || { desc: 'Unknown', icon: '❓' };
setData({
temp: Math.round(current.temperature_2m),
humidity: current.relative_humidity_2m,
windSpeed: Math.round(current.wind_speed_10m),
windDir: current.wind_direction_10m,
description: weather.desc,
icon: weather.icon
});
console.log('[Weather] Loaded:', current.temperature_2m + '°F', weather.desc);
}
} catch (err) {
console.error('Weather fetch error:', err);
setData(null);
} finally {
setLoading(false);
}
};
fetchWeather();
// Refresh every 15 minutes
const interval = setInterval(fetchWeather, 900000);
return () => clearInterval(interval);
}, [location.lat, location.lon]);
return { data, loading };
};
// ============================================
// CONTEST CALENDAR HOOK
// ============================================
@ -2230,6 +2310,7 @@
config, currentTime, utcTime, utcDate, localTime, localDate,
deGrid, dxGrid, deSunTimes, dxSunTimes, dxLocation, onDXChange,
spaceWeather, bandConditions, potaSpots, dxCluster, dxPaths, contests, propagation, mySpots, satellites,
localWeather, use12Hour, onTimeFormatToggle,
onSettingsClick, onFullscreenToggle, isFullscreen
}) => {
const bearing = calculateBearing(config.location.lat, config.location.lon, dxLocation.lat, dxLocation.lon);
@ -2274,7 +2355,7 @@
padding: '2px',
overflow: 'hidden'
}}>
{/* TOP LEFT - Callsign & Time */}
{/* TOP LEFT - Callsign & Weather */}
<div style={{ ...panelStyle, gridRow: '1', gridColumn: '1', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div
style={{ fontSize: '28px', color: 'var(--accent-green)', fontWeight: '900', cursor: 'pointer', fontFamily: 'Orbitron, monospace' }}
@ -2283,9 +2364,15 @@
>
{config.callsign}
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
SFI {spaceWeather.data?.solarFlux || '--'} • K{spaceWeather.data?.kIndex || '-'} • SSN {spaceWeather.data?.sunspotNumber || '--'}
</div>
{localWeather.data ? (
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
{localWeather.data.icon} {localWeather.data.temp}°F • {localWeather.data.description}
</div>
) : (
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
SFI {spaceWeather.data?.solarFlux || '--'} • K{spaceWeather.data?.kIndex || '-'}
</div>
)}
</div>
{/* TOP CENTER - Large Clock */}
@ -2296,7 +2383,11 @@
</div>
<div style={{ width: '1px', height: '40px', background: 'var(--border-color)' }} />
<div style={{ textAlign: 'center' }}>
<div style={{ ...bigValueStyle, color: 'var(--accent-amber)', textShadow: '0 0 10px var(--accent-amber-dim)' }}>{localTime}</div>
<div
style={{ ...bigValueStyle, color: 'var(--accent-amber)', textShadow: '0 0 10px var(--accent-amber-dim)', cursor: 'pointer' }}
onClick={onTimeFormatToggle}
title={`Click to switch to ${use12Hour ? '24-hour' : '12-hour'} format`}
>{localTime}</div>
<div style={{ fontSize: '13px', color: 'var(--text-muted)' }}>{localDate} Local</div>
</div>
</div>
@ -2864,6 +2955,26 @@
const [dxLocation, setDxLocation] = useState(config.defaultDX);
const [showSettings, setShowSettings] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
// 12/24 hour format preference with localStorage persistence
const [use12Hour, setUse12Hour] = useState(() => {
try {
const saved = localStorage.getItem('openhamclock_use12Hour');
return saved === 'true';
} catch (e) { return false; }
});
// Save 12/24 hour preference when changed
useEffect(() => {
try {
localStorage.setItem('openhamclock_use12Hour', use12Hour.toString());
} catch (e) { console.error('Failed to save time format:', e); }
}, [use12Hour]);
// Toggle time format handler
const handleTimeFormatToggle = useCallback(() => {
setUse12Hour(prev => !prev);
}, []);
// Fullscreen toggle handler
const handleFullscreenToggle = useCallback(() => {
@ -2920,6 +3031,7 @@
const propagation = usePropagation(config.location, dxLocation);
const mySpots = useMySpots(config.callsign);
const satellites = useSatellites(config.location);
const localWeather = useLocalWeather(config.location);
const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]);
const dxGrid = useMemo(() => calculateGridSquare(dxLocation.lat, dxLocation.lon), [dxLocation]);
@ -2943,7 +3055,7 @@
}, []);
const utcTime = currentTime.toISOString().substr(11, 8);
const localTime = currentTime.toLocaleTimeString('en-US', { hour12: false });
const localTime = currentTime.toLocaleTimeString('en-US', { hour12: use12Hour });
const utcDate = currentTime.toISOString().substr(0, 10);
const localDate = currentTime.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
@ -2973,6 +3085,9 @@
propagation={propagation}
mySpots={mySpots}
satellites={satellites}
localWeather={localWeather}
use12Hour={use12Hour}
onTimeFormatToggle={handleTimeFormatToggle}
onSettingsClick={() => setShowSettings(true)}
onFullscreenToggle={handleFullscreenToggle}
isFullscreen={isFullscreen}
@ -3065,15 +3180,25 @@
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>{utcDate}</span>
</div>
{/* Local Clock */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{/* Local Clock - Clickable to toggle 12/24 hour format */}
<div
style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}
onClick={handleTimeFormatToggle}
title={`Click to switch to ${use12Hour ? '24-hour' : '12-hour'} format`}
>
<span style={{ fontSize: '12px', color: 'var(--accent-amber)' }}>LOCAL</span>
<span style={{ fontSize: '22px', fontWeight: '700', color: 'var(--accent-amber)', fontFamily: 'Orbitron, monospace' }}>{localTime}</span>
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>{localDate}</span>
</div>
{/* Solar Quick Stats */}
{/* Weather & Solar Stats */}
<div style={{ display: 'flex', gap: '16px', fontSize: '13px' }}>
{localWeather.data && (
<div title={`${localWeather.data.description} Wind: ${localWeather.data.windSpeed} mph`}>
<span style={{ marginRight: '4px' }}>{localWeather.data.icon}</span>
<span style={{ color: 'var(--accent-cyan)', fontWeight: '600' }}>{localWeather.data.temp}°F</span>
</div>
)}
<div><span style={{ color: 'var(--text-muted)' }}>SFI </span><span style={{ color: 'var(--accent-amber)', fontWeight: '600' }}>{spaceWeather.data?.solarFlux || '--'}</span></div>
<div><span style={{ color: 'var(--text-muted)' }}>K </span><span style={{ color: spaceWeather.data?.kIndex >= 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '600' }}>{spaceWeather.data?.kIndex ?? '--'}</span></div>
<div><span style={{ color: 'var(--text-muted)' }}>SSN </span><span style={{ color: 'var(--accent-cyan)', fontWeight: '600' }}>{spaceWeather.data?.sunspotNumber || '--'}</span></div>

Loading…
Cancel
Save

Powered by TurnKey Linux.