pull/54/head
accius 2 days ago
parent 0ef938f87e
commit 3a3f055e02

@ -100,6 +100,9 @@ const App = () => {
const [showDXFilters, setShowDXFilters] = useState(false); const [showDXFilters, setShowDXFilters] = useState(false);
const [showPSKFilters, setShowPSKFilters] = useState(false); const [showPSKFilters, setShowPSKFilters] = useState(false);
const [weatherExpanded, setWeatherExpanded] = useState(false); const [weatherExpanded, setWeatherExpanded] = useState(false);
const [tempUnit, setTempUnit] = useState(() => {
try { return localStorage.getItem('openhamclock_tempUnit') || 'F'; } catch { return 'F'; }
});
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
// Map layer visibility // Map layer visibility
@ -209,7 +212,7 @@ const App = () => {
const propagation = usePropagation(config.location, dxLocation); const propagation = usePropagation(config.location, dxLocation);
const mySpots = useMySpots(config.callsign); const mySpots = useMySpots(config.callsign);
const satellites = useSatellites(config.location); const satellites = useSatellites(config.location);
const localWeather = useLocalWeather(config.location); const localWeather = useLocalWeather(config.location, tempUnit);
const pskReporter = usePSKReporter(config.callsign, { minutes: 15, enabled: config.callsign !== 'N0CALL' }); const pskReporter = usePSKReporter(config.callsign, { minutes: 15, enabled: config.callsign !== 'N0CALL' });
const wsjtx = useWSJTX(); const wsjtx = useWSJTX();
@ -615,29 +618,60 @@ const App = () => {
</div> </div>
{/* Local Weather — compact by default, click to expand */} {/* Local Weather — compact by default, click to expand */}
{localWeather.data && ( {localWeather.data && (() => {
const w = localWeather.data;
const deg = `°${w.tempUnit || tempUnit}`;
const wind = w.windUnit || 'mph';
const vis = w.visUnit || 'mi';
return (
<div style={{ marginTop: '12px', borderTop: '1px solid var(--border-color)', paddingTop: '10px' }}> <div style={{ marginTop: '12px', borderTop: '1px solid var(--border-color)', paddingTop: '10px' }}>
{/* Compact summary row — always visible */} {/* Compact summary row — always visible */}
<div <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
onClick={() => setWeatherExpanded(!weatherExpanded)} <div
style={{ onClick={() => setWeatherExpanded(!weatherExpanded)}
display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', style={{
userSelect: 'none', padding: '2px 0', display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer',
}} userSelect: 'none', flex: 1, minWidth: 0,
> }}
<span style={{ fontSize: '20px', lineHeight: 1 }}>{localWeather.data.icon}</span> >
<span style={{ fontSize: '18px', fontWeight: '700', color: 'var(--text-primary)', fontFamily: 'Orbitron, monospace' }}> <span style={{ fontSize: '20px', lineHeight: 1 }}>{w.icon}</span>
{localWeather.data.temp}°F <span style={{ fontSize: '18px', fontWeight: '700', color: 'var(--text-primary)', fontFamily: 'Orbitron, monospace' }}>
</span> {w.temp}{deg}
<span style={{ fontSize: '11px', color: 'var(--text-secondary)', flex: 1 }}>{localWeather.data.description}</span> </span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)', fontFamily: 'JetBrains Mono, monospace' }}> <span style={{ fontSize: '11px', color: 'var(--text-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{w.description}</span>
💨{localWeather.data.windSpeed} <span style={{ fontSize: '11px', color: 'var(--text-muted)', fontFamily: 'JetBrains Mono, monospace' }}>
</span> 💨{w.windSpeed}
<span style={{ </span>
fontSize: '10px', color: 'var(--text-muted)', <span style={{
transform: weatherExpanded ? 'rotate(180deg)' : 'rotate(0deg)', fontSize: '10px', color: 'var(--text-muted)',
transition: 'transform 0.2s', transform: weatherExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
}}></span> transition: 'transform 0.2s',
}}></span>
</div>
{/* F/C toggle */}
<button
onClick={(e) => {
e.stopPropagation();
const next = tempUnit === 'F' ? 'C' : 'F';
setTempUnit(next);
try { localStorage.setItem('openhamclock_tempUnit', next); } catch {}
}}
style={{
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
color: 'var(--text-secondary)',
fontSize: '10px',
padding: '1px 5px',
borderRadius: '3px',
cursor: 'pointer',
fontFamily: 'JetBrains Mono, monospace',
fontWeight: '600',
flexShrink: 0,
}}
title={`Switch to °${tempUnit === 'F' ? 'C' : 'F'}`}
>
°{tempUnit === 'F' ? 'C' : 'F'}
</button>
</div> </div>
{/* Expanded details */} {/* Expanded details */}
@ -645,14 +679,14 @@ const App = () => {
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
{/* Feels like + hi/lo */} {/* Feels like + hi/lo */}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '8px', fontFamily: 'JetBrains Mono, monospace' }}> <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '8px', fontFamily: 'JetBrains Mono, monospace' }}>
{localWeather.data.feelsLike !== localWeather.data.temp && ( {w.feelsLike !== w.temp && (
<span style={{ color: 'var(--text-muted)' }}>Feels like {localWeather.data.feelsLike}°F</span> <span style={{ color: 'var(--text-muted)' }}>Feels like {w.feelsLike}{deg}</span>
)} )}
{localWeather.data.todayHigh != null && ( {w.todayHigh != null && (
<span style={{ color: 'var(--text-muted)', marginLeft: 'auto' }}> <span style={{ color: 'var(--text-muted)', marginLeft: 'auto' }}>
<span style={{ color: 'var(--accent-amber)' }}>{localWeather.data.todayHigh}°</span> <span style={{ color: 'var(--accent-amber)' }}>{w.todayHigh}°</span>
{' '} {' '}
<span style={{ color: 'var(--accent-blue)' }}>{localWeather.data.todayLow}°</span> <span style={{ color: 'var(--accent-blue)' }}>{w.todayLow}°</span>
</span> </span>
)} )}
</div> </div>
@ -667,50 +701,50 @@ const App = () => {
}}> }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>💨 Wind</span> <span style={{ color: 'var(--text-muted)' }}>💨 Wind</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.windDir} {localWeather.data.windSpeed} mph</span> <span style={{ color: 'var(--text-secondary)' }}>{w.windDir} {w.windSpeed} {wind}</span>
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>💧 Humidity</span> <span style={{ color: 'var(--text-muted)' }}>💧 Humidity</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.humidity}%</span> <span style={{ color: 'var(--text-secondary)' }}>{w.humidity}%</span>
</div> </div>
{localWeather.data.windGusts > localWeather.data.windSpeed + 5 && ( {w.windGusts > w.windSpeed + 5 && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🌬 Gusts</span> <span style={{ color: 'var(--text-muted)' }}>🌬 Gusts</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.windGusts} mph</span> <span style={{ color: 'var(--text-secondary)' }}>{w.windGusts} {wind}</span>
</div> </div>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🌡 Dew Pt</span> <span style={{ color: 'var(--text-muted)' }}>🌡 Dew Pt</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.dewPoint}°F</span> <span style={{ color: 'var(--text-secondary)' }}>{w.dewPoint}{deg}</span>
</div> </div>
{localWeather.data.pressure && ( {w.pressure && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🔵 Pressure</span> <span style={{ color: 'var(--text-muted)' }}>🔵 Pressure</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.pressure} hPa</span> <span style={{ color: 'var(--text-secondary)' }}>{w.pressure} hPa</span>
</div> </div>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}> Clouds</span> <span style={{ color: 'var(--text-muted)' }}> Clouds</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.cloudCover}%</span> <span style={{ color: 'var(--text-secondary)' }}>{w.cloudCover}%</span>
</div> </div>
{localWeather.data.visibility && ( {w.visibility && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>👁 Vis</span> <span style={{ color: 'var(--text-muted)' }}>👁 Vis</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.visibility} mi</span> <span style={{ color: 'var(--text-secondary)' }}>{w.visibility} {vis}</span>
</div> </div>
)} )}
{localWeather.data.uvIndex > 0 && ( {w.uvIndex > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}> UV</span> <span style={{ color: 'var(--text-muted)' }}> UV</span>
<span style={{ color: localWeather.data.uvIndex >= 8 ? '#ef4444' : localWeather.data.uvIndex >= 6 ? '#f97316' : localWeather.data.uvIndex >= 3 ? '#eab308' : 'var(--text-secondary)' }}> <span style={{ color: w.uvIndex >= 8 ? '#ef4444' : w.uvIndex >= 6 ? '#f97316' : w.uvIndex >= 3 ? '#eab308' : 'var(--text-secondary)' }}>
{localWeather.data.uvIndex.toFixed(1)} {w.uvIndex.toFixed(1)}
</span> </span>
</div> </div>
)} )}
</div> </div>
{/* 3-Day Forecast */} {/* 3-Day Forecast */}
{localWeather.data.daily?.length > 0 && ( {w.daily?.length > 0 && (
<div style={{ <div style={{
marginTop: '10px', marginTop: '10px',
paddingTop: '8px', paddingTop: '8px',
@ -718,7 +752,7 @@ const App = () => {
}}> }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '6px', fontWeight: '600' }}>FORECAST</div> <div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '6px', fontWeight: '600' }}>FORECAST</div>
<div style={{ display: 'flex', gap: '4px' }}> <div style={{ display: 'flex', gap: '4px' }}>
{localWeather.data.daily.map((day, i) => ( {w.daily.map((day, i) => (
<div key={i} style={{ <div key={i} style={{
flex: 1, flex: 1,
textAlign: 'center', textAlign: 'center',
@ -747,7 +781,8 @@ const App = () => {
</div> </div>
)} )}
</div> </div>
)} );
})()}
</div> </div>
{/* DX Location */} {/* DX Location */}

@ -43,7 +43,7 @@ function windDirection(deg) {
return dirs[Math.round(deg / 22.5) % 16]; return dirs[Math.round(deg / 22.5) % 16];
} }
export const useLocalWeather = (location) => { export const useLocalWeather = (location, tempUnit = 'F') => {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -52,15 +52,16 @@ export const useLocalWeather = (location) => {
const fetchWeather = async () => { const fetchWeather = async () => {
try { try {
const isMetric = tempUnit === 'C';
const params = [ const params = [
`latitude=${location.lat}`, `latitude=${location.lat}`,
`longitude=${location.lon}`, `longitude=${location.lon}`,
'current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,cloud_cover,pressure_msl,wind_speed_10m,wind_direction_10m,wind_gusts_10m,precipitation,uv_index,visibility,dew_point_2m,is_day', 'current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,cloud_cover,pressure_msl,wind_speed_10m,wind_direction_10m,wind_gusts_10m,precipitation,uv_index,visibility,dew_point_2m,is_day',
'daily=temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,weather_code,sunrise,sunset,uv_index_max,wind_speed_10m_max', 'daily=temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,weather_code,sunrise,sunset,uv_index_max,wind_speed_10m_max',
'hourly=temperature_2m,precipitation_probability,weather_code', 'hourly=temperature_2m,precipitation_probability,weather_code',
'temperature_unit=fahrenheit', `temperature_unit=${isMetric ? 'celsius' : 'fahrenheit'}`,
'wind_speed_unit=mph', `wind_speed_unit=${isMetric ? 'kmh' : 'mph'}`,
'precipitation_unit=inch', `precipitation_unit=${isMetric ? 'mm' : 'inch'}`,
'timezone=auto', 'timezone=auto',
'forecast_days=3', 'forecast_days=3',
'forecast_hours=24', 'forecast_hours=24',
@ -128,7 +129,11 @@ export const useLocalWeather = (location) => {
windGusts: Math.round(current.wind_gusts_10m || 0), windGusts: Math.round(current.wind_gusts_10m || 0),
precipitation: current.precipitation || 0, precipitation: current.precipitation || 0,
uvIndex: current.uv_index || 0, uvIndex: current.uv_index || 0,
visibility: current.visibility ? (current.visibility / 1609.34).toFixed(1) : null, // meters to miles visibility: current.visibility
? isMetric
? (current.visibility / 1000).toFixed(1) // meters to km
: (current.visibility / 1609.34).toFixed(1) // meters to miles
: null,
isDay: current.is_day === 1, isDay: current.is_day === 1,
weatherCode: code, weatherCode: code,
// Today's highs/lows // Today's highs/lows
@ -139,6 +144,10 @@ export const useLocalWeather = (location) => {
daily: dailyForecast, daily: dailyForecast,
// Timezone // Timezone
timezone: result.timezone || '', timezone: result.timezone || '',
// Units
tempUnit: isMetric ? 'C' : 'F',
windUnit: isMetric ? 'km/h' : 'mph',
visUnit: isMetric ? 'km' : 'mi',
}); });
} catch (err) { } catch (err) {
console.error('Weather error:', err); console.error('Weather error:', err);
@ -150,7 +159,7 @@ export const useLocalWeather = (location) => {
fetchWeather(); fetchWeather();
const interval = setInterval(fetchWeather, 15 * 60 * 1000); // 15 minutes const interval = setInterval(fetchWeather, 15 * 60 * 1000); // 15 minutes
return () => clearInterval(interval); return () => clearInterval(interval);
}, [location?.lat, location?.lon]); }, [location?.lat, location?.lon, tempUnit]);
return { data, loading }; return { data, loading };
}; };

Loading…
Cancel
Save

Powered by TurnKey Linux.