pull/54/head
accius 2 days ago
parent 344cff6758
commit 072091ac79

@ -599,7 +599,7 @@ const App = () => {
{/* LEFT SIDEBAR */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflowY: 'auto', overflowX: 'hidden' }}>
{/* DE Location */}
{/* DE Location + Weather */}
<div className="panel" style={{ padding: '14px', flex: '0 0 auto' }}>
<div style={{ fontSize: '14px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '10px' }}>📍 DE - YOUR LOCATION</div>
<div style={{ fontFamily: 'JetBrains Mono', fontSize: '14px' }}>
@ -612,6 +612,122 @@ const App = () => {
<span style={{ color: 'var(--accent-purple)', fontWeight: '600' }}>{deSunTimes.sunset}</span>
</div>
</div>
{/* Local Weather */}
{localWeather.data && (
<div style={{ marginTop: '12px', borderTop: '1px solid var(--border-color)', paddingTop: '12px' }}>
{/* Current conditions hero */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
<span style={{ fontSize: '28px', lineHeight: 1 }}>{localWeather.data.icon}</span>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '6px' }}>
<span style={{ fontSize: '24px', fontWeight: '700', color: 'var(--text-primary)', fontFamily: 'Orbitron, monospace' }}>
{localWeather.data.temp}°F
</span>
{localWeather.data.todayHigh != null && (
<span style={{ fontSize: '11px', color: 'var(--text-muted)', fontFamily: 'JetBrains Mono, monospace' }}>
<span style={{ color: 'var(--accent-amber)' }}>{localWeather.data.todayHigh}°</span>
{' '}
<span style={{ color: 'var(--accent-blue)' }}>{localWeather.data.todayLow}°</span>
</span>
)}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-secondary)' }}>{localWeather.data.description}</div>
{localWeather.data.feelsLike !== localWeather.data.temp && (
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Feels like {localWeather.data.feelsLike}°F</div>
)}
</div>
</div>
{/* Detail grid */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '6px 12px',
fontSize: '11px',
fontFamily: 'JetBrains Mono, monospace',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>💨 Wind</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.windDir} {localWeather.data.windSpeed} mph</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>💧 Humidity</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.humidity}%</span>
</div>
{localWeather.data.windGusts > localWeather.data.windSpeed + 5 && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🌬 Gusts</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.windGusts} mph</span>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🌡 Dew Pt</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.dewPoint}°F</span>
</div>
{localWeather.data.pressure && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🔵 Pressure</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.pressure} hPa</span>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}> Clouds</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.cloudCover}%</span>
</div>
{localWeather.data.visibility && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>👁 Vis</span>
<span style={{ color: 'var(--text-secondary)' }}>{localWeather.data.visibility} mi</span>
</div>
)}
{localWeather.data.uvIndex > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<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)' }}>
{localWeather.data.uvIndex.toFixed(1)}
</span>
</div>
)}
</div>
{/* 3-Day Forecast */}
{localWeather.data.daily?.length > 0 && (
<div style={{
marginTop: '10px',
paddingTop: '8px',
borderTop: '1px solid var(--border-color)',
}}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '6px', fontWeight: '600' }}>FORECAST</div>
<div style={{ display: 'flex', gap: '4px' }}>
{localWeather.data.daily.map((day, i) => (
<div key={i} style={{
flex: 1,
textAlign: 'center',
padding: '6px 2px',
background: 'var(--bg-tertiary)',
borderRadius: '4px',
fontSize: '10px',
}}>
<div style={{ color: 'var(--text-muted)', fontWeight: '600', marginBottom: '2px' }}>{i === 0 ? 'Today' : day.date}</div>
<div style={{ fontSize: '16px', lineHeight: 1.2 }}>{day.icon}</div>
<div style={{ fontFamily: 'JetBrains Mono, monospace', marginTop: '2px' }}>
<span style={{ color: 'var(--accent-amber)' }}>{day.high}°</span>
<span style={{ color: 'var(--text-muted)' }}>/</span>
<span style={{ color: 'var(--accent-blue)' }}>{day.low}°</span>
</div>
{day.precipProb > 0 && (
<div style={{ color: 'var(--accent-blue)', fontSize: '9px', marginTop: '1px' }}>
💧{day.precipProb}%
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
{/* DX Location */}

@ -1,6 +1,6 @@
/**
* useLocalWeather Hook
* Fetches weather data from Open-Meteo API
* Fetches detailed weather data from Open-Meteo API (free, no API key)
*/
import { useState, useEffect } from 'react';
@ -15,9 +15,13 @@ const WEATHER_CODES = {
51: { desc: 'Light drizzle', icon: '🌧️' },
53: { desc: 'Moderate drizzle', icon: '🌧️' },
55: { desc: 'Dense drizzle', icon: '🌧️' },
56: { desc: 'Light freezing drizzle', icon: '🌧️' },
57: { desc: 'Dense freezing drizzle', icon: '🌧️' },
61: { desc: 'Slight rain', icon: '🌧️' },
63: { desc: 'Moderate rain', icon: '🌧️' },
65: { desc: 'Heavy rain', icon: '🌧️' },
66: { desc: 'Light freezing rain', icon: '🌧️' },
67: { desc: 'Heavy freezing rain', icon: '🌧️' },
71: { desc: 'Slight snow', icon: '🌨️' },
73: { desc: 'Moderate snow', icon: '🌨️' },
75: { desc: 'Heavy snow', icon: '❄️' },
@ -28,10 +32,17 @@ const WEATHER_CODES = {
85: { desc: 'Slight snow showers', icon: '🌨️' },
86: { desc: 'Heavy snow showers', icon: '❄️' },
95: { desc: 'Thunderstorm', icon: '⛈️' },
96: { desc: 'Thunderstorm with slight hail', icon: '⛈️' },
99: { desc: 'Thunderstorm with heavy hail', icon: '⛈️' }
96: { desc: 'Thunderstorm w/ slight hail', icon: '⛈️' },
99: { desc: 'Thunderstorm w/ heavy hail', icon: '⛈️' }
};
// Wind direction from degrees
function windDirection(deg) {
if (deg == null) return '';
const dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
return dirs[Math.round(deg / 22.5) % 16];
}
export const useLocalWeather = (location) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
@ -41,21 +52,94 @@ export const useLocalWeather = (location) => {
const fetchWeather = async () => {
try {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}&current=temperature_2m,weather_code,wind_speed_10m&temperature_unit=fahrenheit&wind_speed_unit=mph`;
const params = [
`latitude=${location.lat}`,
`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',
'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',
'temperature_unit=fahrenheit',
'wind_speed_unit=mph',
'precipitation_unit=inch',
'timezone=auto',
'forecast_days=3',
'forecast_hours=24',
].join('&');
const url = `https://api.open-meteo.com/v1/forecast?${params}`;
const response = await fetch(url);
if (response.ok) {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
const code = result.current?.weather_code;
const current = result.current || {};
const code = current.weather_code;
const weather = WEATHER_CODES[code] || { desc: 'Unknown', icon: '🌡️' };
const daily = result.daily || {};
const hourly = result.hourly || {};
// Build hourly forecast (next 24h in 3h intervals)
const hourlyForecast = [];
if (hourly.time && hourly.temperature_2m) {
for (let i = 0; i < Math.min(24, hourly.time.length); i += 3) {
const hCode = hourly.weather_code?.[i];
const hWeather = WEATHER_CODES[hCode] || { desc: '', icon: '🌡️' };
hourlyForecast.push({
time: new Date(hourly.time[i]).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }),
temp: Math.round(hourly.temperature_2m[i]),
precipProb: hourly.precipitation_probability?.[i] || 0,
icon: hWeather.icon,
});
}
}
// Build daily forecast
const dailyForecast = [];
if (daily.time) {
for (let i = 0; i < Math.min(3, daily.time.length); i++) {
const dCode = daily.weather_code?.[i];
const dWeather = WEATHER_CODES[dCode] || { desc: '', icon: '🌡️' };
dailyForecast.push({
date: new Date(daily.time[i] + 'T12:00:00').toLocaleDateString([], { weekday: 'short' }),
high: Math.round(daily.temperature_2m_max?.[i] || 0),
low: Math.round(daily.temperature_2m_min?.[i] || 0),
precipProb: daily.precipitation_probability_max?.[i] || 0,
precipSum: daily.precipitation_sum?.[i] || 0,
icon: dWeather.icon,
desc: dWeather.desc,
windMax: Math.round(daily.wind_speed_10m_max?.[i] || 0),
uvMax: daily.uv_index_max?.[i] || 0,
});
}
}
setData({
temp: Math.round(result.current?.temperature_2m || 0),
// Current conditions
temp: Math.round(current.temperature_2m || 0),
feelsLike: Math.round(current.apparent_temperature || 0),
description: weather.desc,
icon: weather.icon,
windSpeed: Math.round(result.current?.wind_speed_10m || 0),
weatherCode: code
humidity: Math.round(current.relative_humidity_2m || 0),
dewPoint: Math.round(current.dew_point_2m || 0),
pressure: current.pressure_msl ? current.pressure_msl.toFixed(1) : null,
cloudCover: current.cloud_cover || 0,
windSpeed: Math.round(current.wind_speed_10m || 0),
windDir: windDirection(current.wind_direction_10m),
windDirDeg: current.wind_direction_10m || 0,
windGusts: Math.round(current.wind_gusts_10m || 0),
precipitation: current.precipitation || 0,
uvIndex: current.uv_index || 0,
visibility: current.visibility ? (current.visibility / 1609.34).toFixed(1) : null, // meters to miles
isDay: current.is_day === 1,
weatherCode: code,
// Today's highs/lows
todayHigh: daily.temperature_2m_max?.[0] ? Math.round(daily.temperature_2m_max[0]) : null,
todayLow: daily.temperature_2m_min?.[0] ? Math.round(daily.temperature_2m_min[0]) : null,
// Forecasts
hourly: hourlyForecast,
daily: dailyForecast,
// Timezone
timezone: result.timezone || '',
});
}
} catch (err) {
console.error('Weather error:', err);
} finally {

Loading…
Cancel
Save

Powered by TurnKey Linux.