Co-authored-by: terraputix <terraputix@mailbox.org> Reviewed-on: useweb/open-meteo-blue#3
680 lines
22 KiB
Svelte
680 lines
22 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { SvelteDate } from 'svelte/reactivity';
|
|
import { fade } from 'svelte/transition';
|
|
|
|
import { fetchWeatherApi } from 'openmeteo';
|
|
|
|
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
|
|
|
import { pad } from '$lib/utils/index';
|
|
|
|
import { Label } from '$lib/components/ui/label';
|
|
import * as Select from '$lib/components/ui/select';
|
|
|
|
import cloudCover from '../../canvas/cloud-cover';
|
|
import daylight from '../../canvas/daylight';
|
|
import precip from '../../canvas/precip';
|
|
import raster from '../../canvas/raster';
|
|
import tempGradient from '../../canvas/temp-gradient';
|
|
import { defaultParameters, models } from '../../options';
|
|
import { getColor } from '../../utils/colors';
|
|
import weatherCodes from '../../utils/weather-codes';
|
|
|
|
let params = $state({
|
|
latitude: [$storedLocation.latitude],
|
|
longitude: [$storedLocation.longitude],
|
|
models: ['best_match'],
|
|
...defaultParameters
|
|
});
|
|
|
|
let location = $state($storedLocation);
|
|
storedLocation.subscribe((value) => {
|
|
location = value;
|
|
});
|
|
|
|
let diffTemp: number | undefined = $state();
|
|
let maxTemp: number | undefined = $state();
|
|
|
|
let weatherCodesHourly: Float32Array | null | undefined = $state();
|
|
let canvasElement: HTMLCanvasElement | null | undefined = $state();
|
|
|
|
const today = new Date();
|
|
let selectedDay = $state(new Date());
|
|
let selectedDayIndex = $state(1);
|
|
|
|
let entries = $state(0);
|
|
|
|
let weather = $derived(
|
|
(async (location: GeoLocation) => {
|
|
const reqParams = {
|
|
latitude: location.latitude,
|
|
longitude: location.longitude,
|
|
elevation: location.elevation,
|
|
// timezone: location.timezone, ???
|
|
models: [params.models],
|
|
hourly: [
|
|
'precipitation',
|
|
'precipitation_probability',
|
|
'temperature_2m',
|
|
'weather_code',
|
|
'windspeed_10m',
|
|
'winddirection_10m',
|
|
'cloud_cover',
|
|
'relative_humidity_2m'
|
|
].join(','),
|
|
forecast_days: 6,
|
|
past_days: 1,
|
|
temperature_unit: params.temperature_unit,
|
|
wind_speed_unit: params.wind_speed_unit,
|
|
precipitation_unit: params.precipitation_unit
|
|
};
|
|
const url = 'https://api.open-meteo.com/v1/forecast';
|
|
const responses = await fetchWeatherApi(url, reqParams);
|
|
const response = responses[0];
|
|
const utcOffsetSeconds = response.utcOffsetSeconds();
|
|
const hourly = response.hourly()!;
|
|
|
|
weatherCodesHourly = hourly.variables(3)?.valuesArray();
|
|
|
|
let hourlyTime = [
|
|
...Array((Number(hourly.timeEnd()) - Number(hourly.time())) / hourly.interval())
|
|
].map(
|
|
(_, i) =>
|
|
new Date((Number(hourly.time()) + i * hourly.interval() + utcOffsetSeconds) * 1000)
|
|
);
|
|
const hourlyTemps = hourly.variables(2)?.valuesArray();
|
|
const hourlyCloudCover = hourly.variables(6)?.valuesArray();
|
|
const hourlyPrecip = hourly.variables(0)?.valuesArray();
|
|
const indexes = [];
|
|
if (hourlyTemps) {
|
|
for (const index of hourlyTemps.keys()) {
|
|
indexes.push(index);
|
|
}
|
|
}
|
|
|
|
const maxX = 10000;
|
|
const maxY = 500;
|
|
const deltaX = 10000 / (hourly.variables(0)?.valuesArray()?.length || 1);
|
|
|
|
const ctx = canvasElement?.getContext('2d');
|
|
if (ctx) {
|
|
ctx.clearRect(0, 0, maxX, maxY);
|
|
|
|
const minTemp = Math.min(
|
|
...(hourly.variables(2)?.valuesArray() ?? []).filter((t) => !isNaN(t))
|
|
);
|
|
maxTemp = Math.max(...(hourly.variables(2)?.valuesArray() ?? []).filter((t) => !isNaN(t)));
|
|
diffTemp = maxTemp - minTemp;
|
|
|
|
const config: ConfigInterface = {
|
|
maxX: maxX,
|
|
maxY: maxY,
|
|
deltaX: deltaX,
|
|
minTemp: minTemp,
|
|
maxTemp: maxTemp,
|
|
diffTemp: diffTemp
|
|
};
|
|
|
|
// create canvas
|
|
daylight(ctx, config, hourlyTime);
|
|
raster(ctx, config, hourlyTime, today, canvasElement!);
|
|
tempGradient(ctx, config, hourlyTemps, params.temperature_unit);
|
|
cloudCover(ctx, config, hourlyCloudCover, canvasElement!);
|
|
precip(ctx, config, hourlyPrecip, canvasElement!);
|
|
}
|
|
|
|
return {
|
|
entries: [
|
|
{
|
|
id: 0,
|
|
name: 'temperature_2m',
|
|
title: 'Temperature',
|
|
values: hourly
|
|
.variables(2)
|
|
?.valuesArray()
|
|
?.map((t) => Number(t.toFixed(1)))
|
|
},
|
|
{
|
|
id: 1,
|
|
name: 'precipitation',
|
|
title: 'Precipitation',
|
|
values: hourly
|
|
.variables(0)
|
|
?.valuesArray()
|
|
?.map((p) => Number(p.toFixed(1)))
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'precipitation_probability',
|
|
title: 'Precip Prob.',
|
|
values: hourly
|
|
.variables(1)
|
|
?.valuesArray()
|
|
?.map((p) => Number(p.toFixed(0)))
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'windspeed_10m',
|
|
title: 'Wind',
|
|
values: hourly
|
|
.variables(4)
|
|
?.valuesArray()
|
|
?.map((p) => Number(p.toFixed(0)))
|
|
},
|
|
{
|
|
id: 4,
|
|
name: 'relative_humidity_2m',
|
|
title: 'Rel. Hum.',
|
|
values: hourly
|
|
.variables(7)
|
|
?.valuesArray()
|
|
?.map((p) => Number(p.toFixed(0)))
|
|
}
|
|
],
|
|
entriesLength: hourly.variables(0)?.valuesArray()?.length,
|
|
hourlyTime: hourlyTime,
|
|
windDirections: hourly.variables(5)?.valuesArray(),
|
|
indexes: indexes
|
|
};
|
|
})(location)
|
|
);
|
|
|
|
let weatherDaily = $derived(
|
|
(async (location: GeoLocation) => {
|
|
const reqParams = {
|
|
latitude: location.latitude,
|
|
longitude: location.longitude,
|
|
elevation: location.elevation,
|
|
// timezone: location.timezone, ???
|
|
models: [params.models],
|
|
daily: [
|
|
'weather_code',
|
|
'temperature_2m_max',
|
|
'temperature_2m_min',
|
|
'sunrise',
|
|
'sunset',
|
|
'sunshine_duration',
|
|
'precipitation_sum',
|
|
'windspeed_10m_max',
|
|
'windgusts_10m_max',
|
|
'winddirection_10m_dominant'
|
|
].join(','),
|
|
forecast_days: 6,
|
|
past_days: 1,
|
|
temperature_unit: params.temperature_unit,
|
|
wind_speed_unit: params.wind_speed_unit,
|
|
precipitation_unit: params.precipitation_unit
|
|
};
|
|
const url = 'https://api.open-meteo.com/v1/forecast';
|
|
const responses = await fetchWeatherApi(url, reqParams);
|
|
const response = responses[0];
|
|
const utcOffsetSeconds = response.utcOffsetSeconds();
|
|
const daily = response.daily()!;
|
|
|
|
return {
|
|
daily: {
|
|
time: [...Array((Number(daily.timeEnd()) - Number(daily.time())) / daily.interval())].map(
|
|
(_, i) =>
|
|
new Date((Number(daily.time()) + i * daily.interval() + utcOffsetSeconds) * 1000)
|
|
),
|
|
weather_code: daily.variables(0)!,
|
|
temperature_2m_max: daily.variables(1)!,
|
|
temperature_2m_min: daily.variables(2)!,
|
|
sunrise: daily.variables(3)!,
|
|
sunset: daily.variables(4)!,
|
|
sunshine_duration: daily.variables(5)!,
|
|
precipitation_sum: daily.variables(6)!,
|
|
windspeed_10m_max: daily.variables(7)!,
|
|
windgusts_10m_max: daily.variables(8)!,
|
|
winddirection_10m_dominant: daily.variables(9)!
|
|
}
|
|
};
|
|
})(location)
|
|
);
|
|
|
|
let winddir = true;
|
|
entries = 6;
|
|
|
|
let scrollDiv: HTMLElement | undefined = $state();
|
|
let tableCells;
|
|
|
|
const switchDay = (date: Date, index: number) => {
|
|
selectedDay = date;
|
|
|
|
tableCells = document.querySelectorAll('td.time');
|
|
|
|
for (let tableCell of tableCells) {
|
|
const htmlCell = tableCell as HTMLElement;
|
|
if (Number(htmlCell.dataset['date']) === selectedDay.getDate()) {
|
|
scrollDiv?.scrollTo({ left: htmlCell.offsetLeft - 110, behavior: 'smooth' });
|
|
break;
|
|
}
|
|
}
|
|
|
|
selectedDayIndex = index;
|
|
};
|
|
|
|
onMount(() => {
|
|
setTimeout(() => {
|
|
tableCells = document.querySelectorAll('td.time');
|
|
for (let tableCell of tableCells) {
|
|
const htmlCell = tableCell as HTMLElement;
|
|
if (Number(htmlCell.dataset['date']) === selectedDay.getDate()) {
|
|
scrollDiv?.scrollTo({ left: htmlCell.offsetLeft - 110 });
|
|
break;
|
|
}
|
|
}
|
|
}, 150);
|
|
|
|
document.onkeydown = (e) => {
|
|
if (scrollDiv !== document.activeElement && !scrollDiv?.contains(document.activeElement)) {
|
|
if (e.key === 'ArrowLeft') {
|
|
if (selectedDay.getDate() >= today.getDate()) {
|
|
let newDate = new Date();
|
|
newDate.setDate(selectedDay.getDate() - 1);
|
|
switchDay(newDate, selectedDayIndex - 1);
|
|
}
|
|
}
|
|
if (e.key === 'ArrowRight') {
|
|
if (selectedDay.getDate() <= today.getDate() + 4) {
|
|
let newDate = new Date();
|
|
newDate.setDate(selectedDay.getDate() + 1);
|
|
switchDay(newDate, selectedDayIndex + 1);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
let modelSelected = $derived(models.find((mo) => String(mo.value) === params.models?.[0]));
|
|
// let modelSelectedValue = $derived(params.models[0]);
|
|
//
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Weather | Open-Meteo.com</title>
|
|
<link rel="canonical" href="https://open-meteo.com/weather" />
|
|
<meta name="description" content="segseg" />
|
|
</svelte:head>
|
|
|
|
<div class="">
|
|
<div class="weather-content" style="min-height: 50vh">
|
|
<div
|
|
in:fade
|
|
out:fade
|
|
style="min-height: 256px"
|
|
class="weather-week gap-md-2 mb-4 flex flex-col md:flex-row"
|
|
>
|
|
{#await weatherDaily then wd}
|
|
{#each wd.daily.time as time, index (index)}
|
|
{@const selected = time.getDate() === selectedDay.getDate()}
|
|
{#if wd.daily.temperature_2m_max.values(index) != null && !isNaN(Number(wd.daily.temperature_2m_max
|
|
.values(index)!
|
|
.toFixed(1)))}
|
|
<button
|
|
style="transition: 300ms; min-width: 13%; {selected ? 'transform: scale(1.025)' : ''}"
|
|
class="cursor-pointer"
|
|
onclick={() => {
|
|
switchDay(time, index);
|
|
}}
|
|
>
|
|
<div
|
|
class="gap-md-1 flex flex-row items-center justify-center rounded-xl p-1 md:flex-col md:justify-center md:p-3 {selected
|
|
? 'bg-accent'
|
|
: ''}"
|
|
>
|
|
<div class="weather-week-date">
|
|
<b>{time.getDate()} - {time.getMonth() + 1}</b>
|
|
</div>
|
|
|
|
<div
|
|
data-text={time.toLocaleDateString('en-GB', { weekday: 'long' })}
|
|
class="grow-text relative mx-auto inline-flex flex-col {selected
|
|
? 'font-bold'
|
|
: ''}"
|
|
>
|
|
{time.toLocaleDateString('en-GB', { weekday: 'long' })}
|
|
</div>
|
|
|
|
<div class="weather-week-icon pe-none py-2">
|
|
<svg class="fill-foreground" width="60px" height="60px">
|
|
<use
|
|
xlink:href="/images/weather-icons/wi-day-{weatherCodes[
|
|
(wd.daily.weather_code.values(index) ?? 0) as number
|
|
]}.svg#Layer_1"
|
|
></use>
|
|
</svg>
|
|
</div>
|
|
<div
|
|
class="weather-temp-max flex min-w-[65px] justify-center rounded-t p-1 text-sm"
|
|
style={`background-color: ${getColor((wd.daily.temperature_2m_max.values(index) ?? 0).toFixed(0), params.temperature_unit)}; color: ${(wd.daily.temperature_2m_min.values(index) ?? 0) < (params.temperature_unit === 'celsius' ? 4 : 7) || (wd.daily.temperature_2m_min.values(index) ?? 0) >= (params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`}
|
|
>
|
|
{wd.daily.temperature_2m_max.values(index)?.toFixed(1)}
|
|
{params.temperature_unit === 'celsius' ? '°C' : '°F'}
|
|
</div>
|
|
<div
|
|
class="weather-temp-min flex min-w-[65px] justify-center rounded-b p-1 text-sm"
|
|
style={`background: ${getColor((wd.daily.temperature_2m_min.values(index) ?? 0).toFixed(0), params.temperature_unit)}; color: ${(wd.daily.temperature_2m_min.values(index) ?? 0) < (params.temperature_unit === 'celsius' ? 4 : 7) || (wd.daily.temperature_2m_min.values(index) ?? 0) >= (params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`}
|
|
>
|
|
{wd.daily.temperature_2m_min.values(index)?.toFixed(1)}
|
|
{params.temperature_unit === 'celsius' ? '°C' : '°F'}
|
|
</div>
|
|
<div class="mt-2 flex items-center justify-center gap-1">
|
|
<div class="relative flex h-6 w-6 items-center justify-center">
|
|
<div class="absolute">
|
|
<svg class="fill-foreground" width="26px" height="26px">
|
|
<use
|
|
class="stroke-2"
|
|
xlink:href="/images/weather-icons/wi-day-sunny.svg#Layer_1"
|
|
></use>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{Number((wd.daily.sunshine_duration.values(index) ?? 0) / 3600).toFixed(0)}h
|
|
</div>
|
|
<div class="mt-1 flex items-center justify-center">
|
|
<div class="relative flex h-6 w-6 items-center justify-center">
|
|
<div class="absolute">
|
|
<svg class="fill-foreground" width="28px" height="28px">
|
|
<use
|
|
class="stroke-2"
|
|
xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"
|
|
></use>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
{Number(wd.daily.precipitation_sum.values(index)).toFixed(
|
|
1
|
|
)}{params.precipitation_unit === 'mm' ? 'mm' : "'"}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
{/if}
|
|
{/each}
|
|
{:catch error}
|
|
<p style="color: red">{error.message}</p>
|
|
{/await}
|
|
</div>
|
|
<div class="ml-22 md:ml-0">
|
|
<h3 class="text-xl font-bold">
|
|
{selectedDay.toLocaleDateString('en-GB', { weekday: 'long' })}
|
|
<small>
|
|
{selectedDay.getDate() === new Date(today.getTime() - 24 * 60 * 60 * 1000).getDate()
|
|
? ' (Yesterday)'
|
|
: ''}
|
|
{selectedDay.getDate() === today.getDate() ? ' (Today)' : ''}
|
|
{selectedDay.getDate() === new Date(today.getTime() + 24 * 60 * 60 * 1000).getDate()
|
|
? ' (Tomorrow)'
|
|
: ''}
|
|
</small>
|
|
</h3>
|
|
</div>
|
|
<div
|
|
bind:this={scrollDiv}
|
|
style=" height: {218 + entries * 27.5}px; "
|
|
class="w-ful relative -mx-5 overflow-x-scroll overflow-y-hidden md:-ml-[110px]"
|
|
>
|
|
<canvas
|
|
bind:this={canvasElement}
|
|
id="weather_week_canvas"
|
|
class="border border-border"
|
|
style="margin-top: 24px; margin-left: 110px; width: 5000px; height: 200px; "
|
|
height="500px"
|
|
width="10000px"
|
|
></canvas>
|
|
<table in:fade class="absolute bottom-0 border-b border-border">
|
|
<caption style="display:none"> Weather Week {location.name} </caption>
|
|
<tbody>
|
|
{#await weather then weather}
|
|
<tr>
|
|
<th
|
|
scope="row"
|
|
class="time"
|
|
style="color: transparent; padding-left: 4px; background: transparent; left: 0px; min-width: 110px; max-width: 110px; position: sticky;"
|
|
>Time</th
|
|
>
|
|
{#each weather.indexes as index, j (j)}
|
|
<td
|
|
class="time {weather.hourlyTime[index].getDate() === today.getDate() &&
|
|
weather.hourlyTime[index].getHours() === today.getHours()
|
|
? 'now'
|
|
: ''}"
|
|
data-date={weather.hourlyTime[index].getDate()}
|
|
data-time={weather.hourlyTime[index].getHours() + ':00'}
|
|
style="font-size: 11px; position: absolute; bottom: {188 +
|
|
27 * entries}px; left:{111 +
|
|
(5000 / (weather.entriesLength || 1)) * index}px; min-width: {5000 /
|
|
(weather.entriesLength || 1)}px; max-width: {5000 /
|
|
(weather.entriesLength || 1)}px;"
|
|
>{weather.hourlyTime[index].getHours() < 10 ? '0' : ''}{weather.hourlyTime[
|
|
index
|
|
].getHours()}</td
|
|
>
|
|
{/each}
|
|
</tr>
|
|
<!-- icons -->
|
|
<tr>
|
|
<th
|
|
scope="row"
|
|
style="color: transparent; padding-left: 4px; background: transparent; left: 0px; min-width: 110px; max-width: 110px; position: sticky;"
|
|
>Icons</th
|
|
>
|
|
{#each weather.indexes as index, j (j)}
|
|
{@const now =
|
|
weather.hourlyTime[index].getDate() === today.getDate() &&
|
|
weather.hourlyTime[index].getHours() === today.getHours()}
|
|
<td
|
|
style="position: absolute; bottom: {27.5 * entries -
|
|
24 +
|
|
0.8 * 200 -
|
|
0.54 *
|
|
200 *
|
|
((maxTemp! - weather.entries[0].values![index]) / diffTemp!)}px; left:{116 +
|
|
(5000 / (weather.entriesLength || 1)) * index}px; min-width: {5000 /
|
|
(weather.entriesLength || 1)}px; max-width: {5000 /
|
|
(weather.entriesLength || 1)}px;"
|
|
><svg class="fill-foreground {now ? 'scale-125' : ''}" width="20px" height="20px">
|
|
<use
|
|
class="stroke-2"
|
|
xlink:href="/images/weather-icons/wi-{weather.hourlyTime[index].getHours() >
|
|
6 && weather.hourlyTime[index].getHours() < 21
|
|
? 'day'
|
|
: 'night'}-{weatherCodes[weatherCodesHourly![index] as number]}.svg#Layer_1"
|
|
></use>
|
|
</svg></td
|
|
>
|
|
{/each}
|
|
</tr>
|
|
|
|
<!-- min / max -->
|
|
<tr>
|
|
<th
|
|
scope="row"
|
|
style="color: transparent; padding-left: 4px; background: transparent; left: 0px; min-width: 110px; max-width: 110px; position: sticky;"
|
|
>Temp graph</th
|
|
>
|
|
{#each weather.indexes as index, j (j)}
|
|
{@const temp = weather.entries?.[0]?.values?.[index]}
|
|
|
|
{#if temp !== undefined && !isNaN(temp)}
|
|
<td
|
|
class={weather.hourlyTime[index].getDate() === today.getDate() &&
|
|
weather.hourlyTime[index].getHours() === today.getHours()
|
|
? 'now'
|
|
: ''}
|
|
style="position: absolute; bottom: {27.5 * entries -
|
|
49 +
|
|
0.8 * 200 -
|
|
0.55 * 200 * ((maxTemp! - temp!) / diffTemp!)}px; left:{111 +
|
|
(5000 / (weather.entriesLength || 1)) * index}px; min-width: {5000 /
|
|
(weather.entriesLength || 1)}px; max-width: {5000 /
|
|
(weather.entriesLength || 1)}px;">{temp?.toFixed(0)}</td
|
|
>
|
|
{/if}
|
|
{/each}
|
|
</tr>
|
|
{#each weather.entries as entry, i (i)}
|
|
<tr class="border-t border-border">
|
|
<th
|
|
scope="row"
|
|
class="bg-background text-left"
|
|
style="left: 0px; min-width: 110px; max-width: 110px; position: sticky;"
|
|
>{entry.title}</th
|
|
>
|
|
|
|
{#each weather.indexes as index, j (j)}
|
|
{#if entry.values && !isNaN(entry.values[index])}
|
|
<td
|
|
class="border-r border-border {weather.hourlyTime[index].getDate() ===
|
|
today.getDate() && weather.hourlyTime[index].getHours() === today.getHours()
|
|
? 'now'
|
|
: ''}"
|
|
style="min-width: {5000 / (weather.entriesLength || 1)}px; max-width: {5000 /
|
|
(weather.entriesLength || 1)}px;
|
|
{entry.name === 'temperature_2m'
|
|
? 'background: ' +
|
|
getColor(
|
|
weather.entries[0].values![index].toFixed(0),
|
|
params.temperature_unit
|
|
)
|
|
: ''};
|
|
{entry.name === 'temperature_2m'
|
|
? 'color: ' +
|
|
(weather.entries[0].values![index] <
|
|
(params.temperature_unit === 'celsius' ? -13 : 7) ||
|
|
weather.entries[0].values![index] >=
|
|
(params.temperature_unit === 'celsius' ? 40 : 104)
|
|
? 'white'
|
|
: 'black')
|
|
: ''};
|
|
{entry.name === 'precipitation_probability'
|
|
? 'background: rgba(0, 0, 230,' +
|
|
weather.entries[2].values![index] / 120 +
|
|
')'
|
|
: ''};
|
|
{entry.name === 'precipitation_probability'
|
|
? 'color: ' +
|
|
(weather.entries[2].values![index] > 50
|
|
? 'white'
|
|
: 'hsl(var(--foreground)')
|
|
: ''};
|
|
{entry.name === 'relative_humidity_2m'
|
|
? 'background: rgba(0, 240, 240,' +
|
|
weather.entries[4].values![index] ** 3.8 / 10 ** 8.2 +
|
|
')'
|
|
: ''};"
|
|
>{entry.name === 'precipitation' || entry.name === 'temperature_2m'
|
|
? entry.values![index].toFixed(1)
|
|
: entry.values![index]}</td
|
|
>
|
|
{/if}
|
|
{/each}
|
|
</tr>
|
|
{/each}
|
|
{#if winddir}
|
|
<!-- winddir -->
|
|
<tr class="border-t border-border">
|
|
<th
|
|
scope="row"
|
|
class="bg-background text-left"
|
|
style="z-index: 20; left: 0px; min-width: 110px; max-width: 110px; position: sticky;"
|
|
>Wind Dir.</th
|
|
>
|
|
{#each weather.indexes as index, j (j)}
|
|
{#if weather.windDirections && !isNaN(weather.windDirections[index])}
|
|
<td
|
|
class="border-r border-border {weather.hourlyTime[index].getDate() ===
|
|
today.getDate() && weather.hourlyTime[index].getHours() === today.getHours()
|
|
? 'now'
|
|
: ''}"
|
|
style="transform: rotate({weather.windDirections![
|
|
index
|
|
]}deg);min-width: {5000 / (weather.entriesLength || 1)}px; max-width: {5000 /
|
|
(weather.entriesLength || 1)}px;"
|
|
><svg class="fill-foreground" width="25px" height="25px">
|
|
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"></use>
|
|
</svg></td
|
|
>
|
|
{/if}
|
|
{/each}
|
|
</tr>
|
|
{/if}
|
|
{/await}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{#await weatherDaily then wd}
|
|
{@const sunrise = new Date(Number(wd.daily.sunrise.valuesInt64(selectedDayIndex)) * 1000)}
|
|
{@const sunset = new Date(Number(wd.daily.sunset.valuesInt64(selectedDayIndex)) * 1000)}
|
|
<div class="mt-6">
|
|
<div class="flex items-center gap-1">
|
|
<svg class="fill-foreground" width="28px" height="28px">
|
|
<use class="stroke-2" xlink:href="/images/weather-icons/wi-sunrise.svg#Layer_1"></use>
|
|
</svg>Sunrise: {pad(sunrise.getHours())}:{pad(sunrise.getMinutes())}
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<svg class="fill-foreground" width="28px" height="28px">
|
|
<use class="stroke-2" xlink:href="/images/weather-icons/wi-sunset.svg#Layer_1"></use>
|
|
</svg>
|
|
Sunset: {pad(sunset.getHours())}:{pad(sunset.getMinutes())}
|
|
</div>
|
|
</div>
|
|
{/await}
|
|
<div>
|
|
<div class="mt-6 flex gap-6 md:mt-12">
|
|
<div class="relative w-1/2">
|
|
{#if params.models && params.models.length > 0}
|
|
{@const modelValue = params.models[0]}
|
|
<Select.Root
|
|
name="model_selection"
|
|
type="single"
|
|
value={modelValue}
|
|
onValueChange={(val) => {
|
|
if (params.models && val) {
|
|
params.models = [val];
|
|
}
|
|
}}
|
|
>
|
|
<Select.Trigger
|
|
aria-label="Forecast days input"
|
|
class="h-12 cursor-pointer pt-6 [&_svg]:mb-3">{modelSelected?.label}</Select.Trigger
|
|
>
|
|
<Select.Content preventScroll={false} class="border-border">
|
|
{#each models as mo (mo.value)}
|
|
<Select.Item class="cursor-pointer" value={mo.value}>{mo.label}</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
<Label class="absolute top-[0.35rem] left-2 z-10 px-1 text-xs text-muted-foreground"
|
|
>Weather model</Label
|
|
>
|
|
</Select.Root>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.now {
|
|
font-weight: bold;
|
|
}
|
|
|
|
td {
|
|
text-align: center;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.weather-week-icon {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background: #0061a5;
|
|
margin: 5px 0;
|
|
border-radius: 5px;
|
|
}
|
|
</style>
|