weather_app/src/components/WeatherDetail.svelte
2026-07-24 01:53:36 +00:00

296 lines
9.0 KiB
Svelte

<script>
import { app } from '../lib/stores/app.svelte.js'
import { getWeatherInfo } from '../lib/api/weather-codes.js'
const data = $derived(app.forecastData)
const current = $derived(data?.current)
const daily = $derived(data?.daily)
const hourly = $derived(data?.hourly)
const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C')
const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h')
function windCompass(deg) {
if (deg == null) return '--'
const dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
const idx = Math.round(deg / 22.5) % 16
return dirs[idx]
}
function formatTime(iso) {
if (!iso) return '--'
const [h, m] = iso.split('T')[1].split(':')
const hours = parseInt(h, 10)
const ampm = hours >= 12 ? 'PM' : 'AM'
return `${hours % 12 || 12}:${m} ${ampm}`
}
function getBeaufort(speed) {
// Convert to km/h if in imperial (mph)
const kmh = app.settings.units === 'imperial' ? speed * 1.60934 : speed
if (kmh < 2) return 'Calm'
if (kmh < 6) return 'Light air'
if (kmh < 12) return 'Light breeze'
if (kmh < 20) return 'Gentle breeze'
if (kmh < 29) return 'Moderate breeze'
if (kmh < 39) return 'Fresh breeze'
if (kmh < 50) return 'Strong breeze'
if (kmh < 62) return 'High wind'
if (kmh < 75) return 'Gale'
return 'Strong gale'
}
function getUVLevel(uv) {
if (uv == null) return '--'
if (uv <= 2) return 'Low'
if (uv <= 5) return 'Moderate'
if (uv <= 7) return 'High'
if (uv <= 10) return 'Very High'
return 'Extreme'
}
</script>
{#if current}
<div class="detail-section">
<h3 class="section-title">Weather Details</h3>
<div class="detail-grid">
<!-- Wind details -->
<div class="detail-card card">
<div class="detail-header">
<span class="detail-icon">💨</span>
<span class="detail-label">Wind</span>
</div>
<div class="detail-body">
<div class="detail-row">
<span>Speed</span>
<span class="detail-value">{Math.round(current.wind_speed_10m)} {windUnit}</span>
</div>
<div class="detail-row">
<span>Direction</span>
<span class="detail-value">{windCompass(current.wind_direction_10m)} ({current.wind_direction_10m}°)</span>
</div>
<div class="detail-row">
<span>Gusts</span>
<span class="detail-value">{current.wind_gusts_10m != null ? Math.round(current.wind_gusts_10m) + ' ' + windUnit : '--'}</span>
</div>
<div class="detail-row">
<span>Condition</span>
<span class="detail-value">{getBeaufort(current.wind_speed_10m)}</span>
</div>
</div>
</div>
<!-- Atmosphere -->
<div class="detail-card card">
<div class="detail-header">
<span class="detail-icon">🌡️</span>
<span class="detail-label">Atmosphere</span>
</div>
<div class="detail-body">
<div class="detail-row">
<span>Humidity</span>
<span class="detail-value">{current.relative_humidity_2m}%</span>
</div>
<div class="detail-row">
<span>Pressure</span>
<span class="detail-value">{Math.round(current.pressure_msl || 0)} hPa</span>
</div>
<div class="detail-row">
<span>Feels Like</span>
<span class="detail-value">{Math.round(current.apparent_temperature)}{unit}</span>
</div>
<div class="detail-row">
<span>Is Day</span>
<span class="detail-value">{current.is_day ? '☀️ Yes' : '🌙 No'}</span>
</div>
</div>
</div>
<!-- Sun & UV -->
<div class="detail-card card">
<div class="detail-header">
<span class="detail-icon">☀️</span>
<span class="detail-label">Sun & UV</span>
</div>
<div class="detail-body">
<div class="detail-row">
<span>UV Index</span>
<span class="detail-value">
{current.uv_index != null ? Math.round(current.uv_index) : '--'}
{#if current.uv_index != null}
<span class="uv-badge uv-{getUVLevel(current.uv_index).toLowerCase().replace(' ', '-')}">{getUVLevel(current.uv_index)}</span>
{/if}
</span>
</div>
<div class="detail-row">
<span>Sunrise</span>
<span class="detail-value">{formatTime(daily?.sunrise?.[0] || '')}</span>
</div>
<div class="detail-row">
<span>Sunset</span>
<span class="detail-value">{formatTime(daily?.sunset?.[0] || '')}</span>
</div>
<div class="detail-row">
<span>Max UV Today</span>
<span class="detail-value">{daily?.uv_index_max?.[0] != null ? Math.round(daily.uv_index_max[0]) : '--'}</span>
</div>
</div>
</div>
<!-- Temperature range -->
<div class="detail-card card">
<div class="detail-header">
<span class="detail-icon">📊</span>
<span class="detail-label">Temperature Range</span>
</div>
<div class="detail-body">
<div class="detail-row">
<span>Today's High</span>
<span class="detail-value">{daily?.temperature_2m_max?.[0] != null ? Math.round(daily.temperature_2m_max[0]) + unit : '--'}</span>
</div>
<div class="detail-row">
<span>Today's Low</span>
<span class="detail-value">{daily?.temperature_2m_min?.[0] != null ? Math.round(daily.temperature_2m_min[0]) + unit : '--'}</span>
</div>
{#if hourly}
{@const temps = hourly.temperature_2m || []}
{@const validTemps = temps.filter(t => t != null)}
{#if validTemps.length > 0}
<div class="detail-row">
<span>24h Average</span>
<span class="detail-value">{Math.round(validTemps.reduce((a, b) => a + b, 0) / validTemps.length)}{unit}</span>
</div>
{/if}
{/if}
<div class="detail-row">
<span>Precip Chance</span>
<span class="detail-value">{daily?.precipitation_probability_max?.[0] || 0}%</span>
</div>
</div>
</div>
</div>
<!-- Time info -->
<div class="time-info card mt-3">
<div class="time-row">
<span>Local time</span>
<span class="detail-value">{data.timezone || '--'}</span>
</div>
<div class="time-row">
<span>Data updated</span>
<span class="detail-value">{current.time ? new Date(current.time).toLocaleTimeString() : '--'}</span>
</div>
<div class="time-row">
<span>Elevation</span>
<span class="detail-value">{data.elevation != null ? (app.settings.units === 'imperial' ? Math.round(data.elevation * 3.28084) + ' ft' : Math.round(data.elevation) + ' m') : '--'}</span>
</div>
</div>
</div>
{/if}
<style>
.detail-section {
margin-bottom: 4px;
}
.section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 12px;
color: var(--color-text-secondary);
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 10px;
}
.detail-card {
padding: 16px;
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--color-border);
}
.detail-icon {
font-size: 1.1rem;
}
.detail-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--color-text-secondary);
}
.detail-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
color: var(--color-text-muted);
}
.detail-value {
font-weight: 500;
color: var(--color-text);
display: flex;
align-items: center;
gap: 6px;
}
.uv-badge {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}
.uv-badge.low { background: rgba(52, 211, 153, 0.2); color: var(--color-success); }
.uv-badge.moderate { background: rgba(251, 191, 36, 0.2); color: var(--color-sun); }
.uv-badge.high { background: rgba(245, 158, 11, 0.2); color: var(--color-warning); }
.uv-badge.very-high { background: rgba(239, 68, 68, 0.2); color: var(--color-danger); }
.uv-badge.extreme { background: rgba(129, 140, 248, 0.25); color: var(--color-storm); }
/* Time info */
.time-info {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.time-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.82rem;
color: var(--color-text-muted);
}
@media (max-width: 1023px) {
.detail-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.detail-card {
padding: 14px;
}
}
</style>