improve hourly table
This commit is contained in:
@@ -1,15 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { getColor } from '../../utils/colors';
|
||||
import { getTempStyle } from '../../utils/colors';
|
||||
import weatherCodes from '../../utils/weather-codes';
|
||||
import {
|
||||
type FetchedDaily,
|
||||
type WeatherUnits,
|
||||
getDayLabel,
|
||||
getTextColorForTemp,
|
||||
getWindArrowRotation
|
||||
} from './types';
|
||||
import { type FetchedDaily, type WeatherUnits, getDayLabel, getWindArrowRotation } from './types';
|
||||
|
||||
interface Props {
|
||||
daily: FetchedDaily | null;
|
||||
@@ -21,10 +15,32 @@
|
||||
let { daily, selectedDay, units, onSelectDay }: Props = $props();
|
||||
|
||||
const today = new Date();
|
||||
|
||||
function getDaylightSeconds(index: number): number {
|
||||
if (!daily) return 0;
|
||||
const sunriseTs = daily.daily.sunrise[index];
|
||||
const sunsetTs = daily.daily.sunset[index];
|
||||
if (!sunriseTs || !sunsetTs) return 0;
|
||||
return Math.max(0, sunsetTs - sunriseTs);
|
||||
}
|
||||
|
||||
function getSunshinePercent(sunshineSeconds: number | null, daylightSeconds: number): number {
|
||||
if (!sunshineSeconds || daylightSeconds <= 0) return 0;
|
||||
return Math.min(100, (sunshineSeconds / daylightSeconds) * 100);
|
||||
}
|
||||
|
||||
function getSunshineColor(sunshineSeconds: number | null, daylightSeconds: number): string {
|
||||
if (daylightSeconds <= 0) return '#d1d5db';
|
||||
const ratio = (sunshineSeconds ?? 0) / daylightSeconds;
|
||||
if (ratio >= 0.7) return '#f59e0b';
|
||||
if (ratio >= 0.45) return '#fbbf24';
|
||||
if (ratio >= 0.2) return '#fcd34d';
|
||||
return '#d1d5db';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div in:fade out:fade class="daily-cards-wrapper">
|
||||
<div class="daily-cards-scroll">
|
||||
<div in:fade out:fade class="mb-6 min-h-[260px]">
|
||||
<div class="flex gap-1 overflow-x-auto pb-1" style="scrollbar-width: thin">
|
||||
{#if daily}
|
||||
{#each daily.dailyDates as time, index (index)}
|
||||
{@const selected = time.getDate() === selectedDay.getDate()}
|
||||
@@ -32,22 +48,38 @@
|
||||
{@const tempMin = daily.daily.temperature_2m_min[index]}
|
||||
{@const wCode = daily.daily.weather_code[index]}
|
||||
{@const sunDuration = daily.daily.sunshine_duration[index]}
|
||||
{@const daylightSec = getDaylightSeconds(index)}
|
||||
{@const sunColor = getSunshineColor(sunDuration, daylightSec)}
|
||||
{@const sunPct = getSunshinePercent(sunDuration, daylightSec)}
|
||||
{@const precipSum = daily.daily.precipitation_sum[index]}
|
||||
{@const windMax = daily.daily.windspeed_10m_max[index]}
|
||||
{@const gustMax = daily.daily.windgusts_10m_max[index]}
|
||||
{@const windDir = daily.daily.winddirection_10m_dominant[index]}
|
||||
{@const unit = String(units.temperature_unit)}
|
||||
{@const maxStyle = getTempStyle(tempMax, unit)}
|
||||
{@const minStyle = getTempStyle(tempMin, unit)}
|
||||
{#if tempMax != null && !isNaN(tempMax)}
|
||||
<button
|
||||
class="daily-card {selected ? 'selected' : ''}"
|
||||
class="group flex min-w-[108px] max-w-[130px] flex-1 cursor-pointer flex-col items-center gap-0.5 rounded-xl border-2 px-1.5 py-2 transition-all duration-200
|
||||
{selected
|
||||
? 'scale-[1.03] border-primary bg-accent shadow-md'
|
||||
: 'border-transparent bg-card hover:bg-accent'}"
|
||||
onclick={() => onSelectDay(time, index)}
|
||||
>
|
||||
<div class="card-day-name">
|
||||
<!-- Day label -->
|
||||
<span class="text-sm font-bold tracking-wide">
|
||||
{time.toLocaleDateString('en-GB', { weekday: 'short' }).toUpperCase()}
|
||||
</div>
|
||||
<div class="card-day-date">{getDayLabel(time, today)}</div>
|
||||
</span>
|
||||
<span class="text-[11px] text-muted-foreground">
|
||||
{getDayLabel(time, today)}
|
||||
</span>
|
||||
|
||||
<div class="card-icon">
|
||||
<svg class="fill-foreground" width="52px" height="52px">
|
||||
<!-- Weather icon -->
|
||||
<div
|
||||
class="my-1 flex w-full items-center justify-center rounded-lg py-1.5"
|
||||
style="background: {sunColor}22"
|
||||
>
|
||||
<svg class="fill-foreground" width="48px" height="48px">
|
||||
<use
|
||||
xlink:href="/images/weather-icons/wi-day-{weatherCodes[
|
||||
wCode as keyof typeof weatherCodes
|
||||
@@ -56,57 +88,75 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card-temp-max"
|
||||
style="background-color: {getColor(
|
||||
tempMax,
|
||||
String(units.temperature_unit)
|
||||
)}; color: {getTextColorForTemp(tempMax, String(units.temperature_unit))}"
|
||||
>
|
||||
{tempMax.toFixed(0)}{units.temperature_unit === 'celsius' ? '°C' : '°F'}
|
||||
</div>
|
||||
<div
|
||||
class="card-temp-min"
|
||||
style="background-color: {getColor(
|
||||
tempMin,
|
||||
String(units.temperature_unit)
|
||||
)}; color: {getTextColorForTemp(tempMin, String(units.temperature_unit))}"
|
||||
>
|
||||
{tempMin.toFixed(0)}{units.temperature_unit === 'celsius' ? '°C' : '°F'}
|
||||
</div>
|
||||
|
||||
<div class="card-detail">
|
||||
<svg class="fill-foreground detail-icon" width="16px" height="16px">
|
||||
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
|
||||
</svg>
|
||||
<span>{windMax?.toFixed(0) ?? '-'}-{gustMax?.toFixed(0) ?? '-'}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-detail">
|
||||
<svg class="fill-foreground detail-icon" width="16px" height="16px">
|
||||
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{Number(precipSum ?? 0).toFixed(0)}{units.precipitation_unit === 'mm' ? ' mm' : "'"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card-detail">
|
||||
<svg class="fill-foreground detail-icon" width="16px" height="16px">
|
||||
<use xlink:href="/images/weather-icons/wi-day-sunny.svg#Layer_1"></use>
|
||||
</svg>
|
||||
<span>{Number((sunDuration ?? 0) / 3600).toFixed(0)} h</span>
|
||||
</div>
|
||||
|
||||
{#if windDir != null && !isNaN(windDir)}
|
||||
<div class="card-wind-dir">
|
||||
<div style="transform: {getWindArrowRotation(windDir)}; display: inline-block">
|
||||
<svg class="fill-foreground" width="18px" height="18px">
|
||||
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Temperature max/min -->
|
||||
<div class="flex w-full flex-col">
|
||||
<div
|
||||
class="w-full rounded-t px-1 py-0.5 text-center text-[15px] font-bold"
|
||||
style="background-color: {maxStyle.bg}; color: {maxStyle.fg}"
|
||||
>
|
||||
{tempMax.toFixed(0)}°
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="w-full rounded-b px-1 py-0.5 text-center text-xs font-semibold"
|
||||
style="background-color: {minStyle.bg}; color: {minStyle.fg}"
|
||||
>
|
||||
{tempMin.toFixed(0)}°
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details section -->
|
||||
<div class="mt-1 flex w-full flex-col items-center gap-0.5">
|
||||
<!-- Sunshine bar -->
|
||||
<div class="flex w-full items-center gap-1 px-1">
|
||||
<svg class="shrink-0" width="14px" height="14px" style="fill: {sunColor}">
|
||||
<use xlink:href="/images/weather-icons/wi-day-sunny.svg#Layer_1"></use>
|
||||
</svg>
|
||||
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
class="h-full rounded-full transition-all"
|
||||
style="width: {sunPct}%; background-color: {sunColor}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-[10px] font-medium text-muted-foreground">
|
||||
{Number((sunDuration ?? 0) / 3600).toFixed(0)}h
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Precipitation -->
|
||||
<div class="flex items-center gap-1 text-[11px]">
|
||||
<svg class="fill-foreground shrink-0" width="14px" height="14px">
|
||||
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{Number(precipSum ?? 0).toFixed(
|
||||
precipSum >= 10 ? 0 : 1
|
||||
)}{units.precipitation_unit === 'mm' ? ' mm' : "'"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Wind with direction -->
|
||||
<div class="flex items-center gap-1 text-[11px]">
|
||||
{#if windDir != null && !isNaN(windDir)}
|
||||
<div
|
||||
class="inline-flex shrink-0"
|
||||
style="transform: {getWindArrowRotation(windDir)}"
|
||||
>
|
||||
<svg class="fill-foreground" width="20px" height="20px">
|
||||
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"></use>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<svg class="fill-foreground shrink-0" width="20px" height="20px">
|
||||
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
|
||||
</svg>
|
||||
{/if}
|
||||
<span>
|
||||
{windMax?.toFixed(0) ?? '-'}<span class="text-muted-foreground"
|
||||
>-{gustMax?.toFixed(0) ?? '-'}</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -115,118 +165,14 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.daily-cards-wrapper {
|
||||
margin-bottom: 1.5rem;
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.daily-cards-scroll {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.daily-card {
|
||||
flex: 1;
|
||||
min-width: 110px;
|
||||
max-width: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 6px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
background: hsl(var(--card));
|
||||
transition: all 200ms ease;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.daily-card:hover {
|
||||
background: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.daily-card.selected {
|
||||
border-color: hsl(var(--primary));
|
||||
background: hsl(var(--accent));
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-day-name {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-day-date {
|
||||
font-size: 11px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 4px 0;
|
||||
background: #0061a5;
|
||||
border-radius: 6px;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.card-temp-max {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
padding: 3px 0;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.card-temp-min {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
padding: 3px 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-wind-dir {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.daily-card {
|
||||
min-width: 95px;
|
||||
max-width: 110px;
|
||||
button {
|
||||
min-width: 92px !important;
|
||||
}
|
||||
|
||||
.card-icon svg {
|
||||
button :global(svg[width='48px']) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.card-temp-max,
|
||||
.card-temp-min {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user