This repository has been archived on 2026-08-10. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
drizzli/src/routes/weather/week/[location]/DailyCards.svelte
T
2026-02-16 00:32:26 +01:00

179 lines
6.2 KiB
Svelte

<script lang="ts">
import { fade } from 'svelte/transition';
import { getTempStyle } from '../../utils/colors';
import weatherCodes from '../../utils/weather-codes';
import { type FetchedDaily, type WeatherUnits, getDayLabel, getWindArrowRotation } from './types';
interface Props {
daily: FetchedDaily | null;
selectedDay: Date;
units: WeatherUnits;
onSelectDay: (date: Date, index: number) => void;
}
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="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()}
{@const tempMax = daily.daily.temperature_2m_max[index]}
{@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="group flex min-w-[108px] max-w-[170px] 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)}
>
<!-- Day label -->
<span class="text-sm font-bold tracking-wide">
{time.toLocaleDateString('en-GB', { weekday: 'short' }).toUpperCase()}
</span>
<span class="text-[11px] text-muted-foreground">
{getDayLabel(time, today)}
</span>
<!-- 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
] ?? 'clear'}.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>
<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}
{/if}
</div>
</div>
<style>
@media (max-width: 768px) {
button {
min-width: 92px !important;
}
button :global(svg[width='48px']) {
width: 40px;
height: 40px;
}
}
</style>