296 lines
9.0 KiB
Svelte
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>
|