gemini going wild on this

This commit is contained in:
terraputix
2026-02-15 14:51:15 +01:00
parent 298c8ef597
commit ba9834a3eb
28 changed files with 556 additions and 477 deletions
+1 -3
View File
@@ -1,5 +1,3 @@
{ {
"userWords": [ "userWords": ["ConfigInterface"]
"ConfigInterface"
]
} }
@@ -240,8 +240,8 @@
{location.country || ''} {location.country || ''}
</p> </p>
<p class="text-xs text-gray-500 dark:text-gray-400"> <p class="text-xs text-gray-500 dark:text-gray-400">
{location.latitude.toFixed(2)}°N {location.longitude.toFixed(2)}°E {location.latitude?.toFixed(2)}°N {location.longitude?.toFixed(2)}°E
{#if location.elevation}{location.elevation.toFixed(0)}m{/if} {#if location.elevation}{location.elevation?.toFixed(0)}m{/if}
</p> </p>
</div> </div>
</div> </div>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils/index.js';
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,7 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="card-action" data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)} class={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils/index.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -10,6 +10,6 @@
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}> <div bind:this={ref} data-slot="card-content" class={cn('px-6', className)} {...restProps}>
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils/index.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,7 +13,7 @@
<p <p
bind:this={ref} bind:this={ref}
data-slot="card-description" data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)} class={cn('text-sm text-muted-foreground', className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils/index.js';
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,7 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="card-footer" data-slot="card-footer"
class={cn("flex items-center px-6 [.border-t]:pt-6", className)} class={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils/index.js';
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -14,7 +14,7 @@
bind:this={ref} bind:this={ref}
data-slot="card-header" data-slot="card-header"
class={cn( class={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className className
)} )}
{...restProps} {...restProps}
+3 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils/index.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,7 +13,7 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="card-title" data-slot="card-title"
class={cn("leading-none font-semibold", className)} class={cn('leading-none font-semibold', className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
+3 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils/index.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -14,7 +14,7 @@
bind:this={ref} bind:this={ref}
data-slot="card" data-slot="card"
class={cn( class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", 'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm',
className className
)} )}
{...restProps} {...restProps}
+8 -8
View File
@@ -1,10 +1,10 @@
import Root from "./card.svelte"; import Root from './card.svelte';
import Content from "./card-content.svelte"; import Content from './card-content.svelte';
import Description from "./card-description.svelte"; import Description from './card-description.svelte';
import Footer from "./card-footer.svelte"; import Footer from './card-footer.svelte';
import Header from "./card-header.svelte"; import Header from './card-header.svelte';
import Title from "./card-title.svelte"; import Title from './card-title.svelte';
import Action from "./card-action.svelte"; import Action from './card-action.svelte';
export { export {
Root, Root,
@@ -21,5 +21,5 @@ export {
Footer as CardFooter, Footer as CardFooter,
Header as CardHeader, Header as CardHeader,
Title as CardTitle, Title as CardTitle,
Action as CardAction, Action as CardAction
}; };
+18 -18
View File
@@ -1,24 +1,24 @@
import { persisted } from 'svelte-persisted-store'; import { persisted } from 'svelte-persisted-store';
export interface GeoLocation { export interface GeoLocation {
id: number; id?: number;
name: string; name?: string;
latitude: number; latitude?: number;
longitude: number; longitude?: number;
elevation: number; elevation?: number;
feature_code: string; feature_code?: string;
country_code: string | undefined; country_code?: string;
admin1_id: number | undefined; admin1_id?: number;
admin3_id?: number | undefined; admin3_id?: number;
admin4_id?: number | undefined; admin4_id?: number;
timezone: string; timezone?: string;
population: number | undefined; population?: number;
postcodes: string[] | undefined; postcodes?: string[];
country_id: number | undefined; country_id?: number;
country: string | undefined; country?: string;
admin1: string | undefined; admin1?: string;
admin3?: string | undefined; admin3?: string;
admin4?: string | undefined; admin4?: string;
} }
export const defaultLocation: GeoLocation = { export const defaultLocation: GeoLocation = {
+5 -5
View File
@@ -6,7 +6,7 @@ import { page } from '$app/state';
import { debounce, isNumeric } from '$lib/utils'; import { debounce, isNumeric } from '$lib/utils';
import type { Parameters } from '$lib/docs'; import type { Parameters } from '$lib/types';
export type UrlHashStore = Writable<Parameters>; export type UrlHashStore = Writable<Parameters>;
@@ -44,20 +44,20 @@ export const urlHashStore = (initialValues: Parameters): UrlHashStore => {
changedParams = true; changedParams = true;
} }
} else { } else {
let array = value; let array = value as any[];
// remove empty string when array has more then 1 values // remove empty string when array has more then 1 values
if (array.length > 1 && array.includes('')) { if (array.length > 1 && array.includes('')) {
array = value.filter((e: string) => e !== ''); array = array.filter((e: string) => e !== '');
} }
page.url.searchParams.set(key, array.join(',')); page.url.searchParams.set(key, array.join(','));
changedParams = true; changedParams = true;
} }
} else { } else {
let val: number | string = value; let val: number | string = value as number | string;
if (isNumeric(defaultValue)) { if (isNumeric(defaultValue)) {
defaultValue = Number(defaultValue); defaultValue = Number(defaultValue);
} }
if (isNumeric(value)) { if (isNumeric(value as number | string)) {
val = Number(value); val = Number(value);
} }
+30
View File
@@ -0,0 +1,30 @@
export interface Parameters {
latitude?: number | number[];
longitude?: number | number[];
hourly?: string[];
models?: string[];
daily?: string[];
current?: string[];
minutely_15?: string[];
timezone?: string;
location_mode?: string;
csv_coordinates?: string;
time_mode?: string;
past_days?: string;
forecast_days?: string;
end_date?: string;
start_date?: string;
past_hours?: string;
cell_selection?: string;
forecast_hours?: string;
past_minutely_15?: string;
temporal_resolution?: string;
forecast_minutely_15?: string;
tilt?: string;
azimuth?: string;
timeformat?: string;
wind_speed_unit?: string;
temperature_unit?: string;
precipitation_unit?: string;
[key: string]: any;
}
+3
View File
@@ -1,3 +1,6 @@
export * from './ui.js';
export * from './meteo.js';
export const isNumeric = (num: string | number) => export const isNumeric = (num: string | number) =>
(typeof num === 'number' || (typeof num === 'string' && num.trim() !== '')) && (typeof num === 'number' || (typeof num === 'string' && num.trim() !== '')) &&
!isNaN(num as number); !isNaN(num as number);
+1
View File
@@ -0,0 +1 @@
export const prerender = true;
+4 -2
View File
@@ -13,7 +13,7 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store';
let mounted = $state(false); let mounted = $state(false);
let location = get(storedLocation); let location = $derived($storedLocation);
onMount(() => { onMount(() => {
mounted = true; mounted = true;
@@ -180,7 +180,9 @@
Current Location: {location.name} Current Location: {location.name}
</CardTitle> </CardTitle>
<CardDescription class="text-center"> <CardDescription class="text-center">
{location.country}{location.latitude.toFixed(2)}°, {location.longitude.toFixed(2)}° {location.country}{location.latitude?.toFixed(2)}°, {location.longitude?.toFixed(
2
)}°
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent class="text-center"> <CardContent class="text-center">
+2 -2
View File
@@ -83,8 +83,8 @@
{/if}{location.country} {/if}{location.country}
</p> </p>
<p class="text-sm text-gray-500 dark:text-gray-400"> <p class="text-sm text-gray-500 dark:text-gray-400">
{location.latitude.toFixed(2)}°N, {location.longitude.toFixed(2)}°E {location.latitude?.toFixed(2)}°N, {location.longitude?.toFixed(2)}°E
{#if location.elevation}{location.elevation.toFixed(0)}m{/if} {#if location.elevation}{location.elevation?.toFixed(0)}m{/if}
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -2,7 +2,7 @@ import { get } from 'svelte/store';
import { storedLocation } from '$lib/stores/settings'; import { storedLocation } from '$lib/stores/settings';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from '././$types';
const location = get(storedLocation); const location = get(storedLocation);
+3 -3
View File
@@ -1,8 +1,8 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { PageLoad } from '$types'; import type { PageLoad } from './$types';
export const prerender = true; export const prerender = true;
export const load = (async () => { export const load: PageLoad = async () => {
throw redirect(303, '/weather/week/'); throw redirect(303, '/weather/week/');
}) satisfies PageLoad; };
+1 -1
View File
@@ -2,7 +2,7 @@ import { get } from 'svelte/store';
import { storedLocation } from '$lib/stores/settings'; import { storedLocation } from '$lib/stores/settings';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from '././$types';
const location = get(storedLocation); const location = get(storedLocation);
+36 -21
View File
@@ -15,8 +15,9 @@
import { defaultParameters } from './options'; import { defaultParameters } from './options';
let node: HTMLElement; let node: HTMLElement;
let chart: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any
let Highcharts = $state(null); let charts: any[] = [];
let Highcharts = $state<any>(null);
let showLegend = $state(false); let showLegend = $state(false);
let averageOnly = $state(false); let averageOnly = $state(false);
@@ -36,41 +37,49 @@
/// Highcharts needs to be loaded in `onMount` to work with prerendered SSG /// Highcharts needs to be loaded in `onMount` to work with prerendered SSG
Highcharts = (await import('highcharts')).default; Highcharts = (await import('highcharts')).default;
const more = (await import('highcharts/highcharts-more')).default; const more = (await import('highcharts/highcharts-more')).default;
// more(Highcharts); (more as any)(Highcharts);
if (dev) { if (dev) {
// const HighchartsDebugger = await import('highcharts/modules/debugger'); // const HighchartsDebugger = await import('highcharts/modules/debugger');
// HighchartsDebugger.default(Highcharts); // HighchartsDebugger.default(Highcharts);
const Debugger = (await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) // @ts-ignore
.default; const Debugger = (
(await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) as any
).default;
// @ts-ignore
const ErrorMessages = ( const ErrorMessages = (
await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js') (await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js')) as any
).default; ).default;
Highcharts.errorMessages = ErrorMessages; Highcharts.errorMessages = ErrorMessages;
Debugger.compose(Highcharts.Chart); Debugger.compose(Highcharts.Chart);
} }
}); });
$effect(async () => { $effect(() => {
count = 0; count = 0;
if (Highcharts) { if (Highcharts) {
node.replaceChildren([]); charts.forEach((c) => c.destroy());
charts = [];
// eslint-disable-next-line svelte/no-dom-manipulating
(node as any).replaceChildren();
(async () => {
const dataDaily = await fetch( const dataDaily = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&timeformat=unixtime&daily=sunset,sunrise&forecast_days=14` `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&timeformat=unixtime&daily=sunset,sunrise&forecast_days=14`
); );
const wd = await dataDaily.json(); const wd = await dataDaily.json();
const dataReq = await fetch( const dataReq = await fetch(
`https://ensemble-api.open-meteo.com/v1/ensemble?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly.join(',')}&models=${$params.models.join(',')}&timeformat=unixtime&forecast_days=14` `https://ensemble-api.open-meteo.com/v1/ensemble?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly?.join(',')}&models=${$params.models?.join(',')}&timeformat=unixtime&forecast_days=14`
); );
const data = await dataReq.json(); const data = await dataReq.json();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let plotBands: any = []; let plotBands: any = [];
if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) { if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) {
let rise = wd.daily.sunrise; let rise = wd.daily.sunrise;
let set = wd.daily.sunset; let set = wd.daily.sunset;
plotBands = rise.map(function (r, i) { plotBands = rise.map(function (r: any, i: number) {
return { return {
color: 'rgba(255, 255, 194, 0.5)', color: 'rgba(255, 255, 194, 0.5)',
from: (r + data.utc_offset_seconds) * 1000, from: (r + data.utc_offset_seconds) * 1000,
@@ -82,7 +91,7 @@
let minValues = new Array(data.hourly.time.length).fill(undefined); let minValues = new Array(data.hourly.time.length).fill(undefined);
let maxValues = new Array(data.hourly.time.length).fill(undefined); let maxValues = new Array(data.hourly.time.length).fill(undefined);
for (let variable of $params.hourly) { for (let variable of $params.hourly || []) {
const chartDiv = document.createElement('div'); const chartDiv = document.createElement('div');
let unit; let unit;
@@ -99,7 +108,7 @@
continue; continue;
} }
if (model.startsWith(variable)) { if (model.startsWith(variable)) {
for (let [index, val] of values.entries()) { for (let [index, val] of (values as any[]).entries()) {
if (val) { if (val) {
let avVal = average[index]; let avVal = average[index];
average[index] = avVal + val; average[index] = avVal + val;
@@ -145,7 +154,9 @@
dashStyle: 'ShortDashDot', dashStyle: 'ShortDashDot',
color: '#5e5e5e', color: '#5e5e5e',
type: type:
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' ? 'column' : 'spline', unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'
? 'column'
: 'spline',
tooltip: { tooltip: {
valueSuffix: ' ' + unit valueSuffix: ' ' + unit
}, },
@@ -160,9 +171,9 @@
className: 'highcharts-average-series' className: 'highcharts-average-series'
}); });
new Highcharts.Chart(chartDiv, { const chart = new Highcharts.Chart(chartDiv, {
credits: { credits: {
text: count === $params.hourly.length - 1 ? 'Open-Meteo.com' : '', text: count === ($params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '',
href: 'http://open-meteo.com' href: 'http://open-meteo.com'
}, },
@@ -185,8 +196,8 @@
subtitle: { subtitle: {
text: text:
count === 0 count === 0
? `Compare <span class="font-bold">${$params.hourly.join(', ')}</span> in models: <span class="font-bold">` + ? `Compare <span class="font-bold">${$params.hourly?.join(', ')}</span> in models: <span class="font-bold">` +
$params.models.join(', ') + $params.models?.join(', ') +
'</span>' '</span>'
: '', : '',
align: 'left' align: 'left'
@@ -253,20 +264,24 @@
}); });
count++; count++;
charts.push(chart);
// eslint-disable-next-line svelte/no-dom-manipulating
node.appendChild(chartDiv); node.appendChild(chartDiv);
} }
})();
} }
}); });
onDestroy(() => { onDestroy(() => {
if (chart) { charts.forEach((c) => c.destroy());
chart.destroy();
}
}); });
</script> </script>
<!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] --> <!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] -->
<div class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * $params.hourly.length + 2}px]"> <div
class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * ($params.hourly?.length || 0) +
2}px]"
>
<div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div> <div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div>
<div <div
class="{count > 0 class="{count > 0
+1 -1
View File
@@ -2,7 +2,7 @@ import { get } from 'svelte/store';
import { storedLocation } from '$lib/stores/settings'; import { storedLocation } from '$lib/stores/settings';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from '././$types';
const location = get(storedLocation); const location = get(storedLocation);
+44 -29
View File
@@ -17,8 +17,9 @@
import { defaultParameters } from './options'; import { defaultParameters } from './options';
let node: HTMLElement; let node: HTMLElement;
let chart: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any
let Highcharts = $state(); let charts: any[] = [];
let Highcharts = $state<any>();
let showLegend = $state(false); let showLegend = $state(false);
let averageOnly = $state(false); let averageOnly = $state(false);
@@ -47,30 +48,38 @@
if (dev) { if (dev) {
// const HighchartsDebugger = await import('highcharts/modules/debugger'); // const HighchartsDebugger = await import('highcharts/modules/debugger');
// HighchartsDebugger.default(Highcharts); // HighchartsDebugger.default(Highcharts);
const Debugger = (await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) // @ts-ignore
.default; const Debugger = (
(await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) as any
).default;
// @ts-ignore
const ErrorMessages = ( const ErrorMessages = (
await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js') (await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js')) as any
).default; ).default;
Highcharts.errorMessages = ErrorMessages; Highcharts.errorMessages = ErrorMessages;
Debugger.compose(Highcharts.Chart); Debugger.compose(Highcharts.Chart);
} }
}); });
$effect(async () => { $effect(() => {
count = 0; count = 0;
if (Highcharts) { if (Highcharts) {
node.replaceChildren([]); charts.forEach((c) => c.destroy());
charts = [];
// eslint-disable-next-line svelte/no-dom-manipulating
(node as any).replaceChildren();
(async () => {
const dataReq = await fetch( const dataReq = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly.join(',')}&models=${$params.models.join(',')}&timeformat=unixtime&daily=sunset,sunrise` `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly?.join(',')}&models=${$params.models?.join(',')}&timeformat=unixtime&daily=sunset,sunrise`
); );
const data = await dataReq.json(); const data = await dataReq.json();
let dailyFirstModelKey = Object.keys(data.daily)[1].split('_'); let dailyFirstModelKeyParts = Object.keys(data.daily)[1].split('_');
dailyFirstModelKey.shift(); dailyFirstModelKeyParts.shift();
dailyFirstModelKey = dailyFirstModelKey.join('_'); const dailyFirstModelKey = dailyFirstModelKeyParts.join('_');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let plotBands: any = []; let plotBands: any = [];
if ( if (
'daily' in data && 'daily' in data &&
@@ -79,7 +88,7 @@
) { ) {
let rise = data.daily['sunrise_' + dailyFirstModelKey]; let rise = data.daily['sunrise_' + dailyFirstModelKey];
let set = data.daily['sunset_' + dailyFirstModelKey]; let set = data.daily['sunset_' + dailyFirstModelKey];
plotBands = rise.map(function (r, i) { plotBands = rise.map(function (r: any, i: number) {
return { return {
color: 'rgba(255, 255, 194, 0.5)', color: 'rgba(255, 255, 194, 0.5)',
from: (r + data.utc_offset_seconds) * 1000, from: (r + data.utc_offset_seconds) * 1000,
@@ -88,7 +97,7 @@
}); });
} }
for (let variable of $params.hourly) { for (let variable of $params.hourly || []) {
const chartDiv = document.createElement('div'); const chartDiv = document.createElement('div');
let unit; let unit;
@@ -105,7 +114,7 @@
continue; continue;
} }
if (model.startsWith(variable)) { if (model.startsWith(variable)) {
for (let [index, val] of values.entries()) { for (let [index, val] of (values as any[]).entries()) {
if (val) { if (val) {
let avVal = average[index]; let avVal = average[index];
average[index] = avVal + val; average[index] = avVal + val;
@@ -143,7 +152,9 @@
dashStyle: 'ShortDashDot', dashStyle: 'ShortDashDot',
color: '#5e5e5e', color: '#5e5e5e',
type: type:
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' ? 'column' : 'spline', unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'
? 'column'
: 'spline',
tooltip: { tooltip: {
valueSuffix: ' ' + unit valueSuffix: ' ' + unit
}, },
@@ -158,9 +169,9 @@
className: 'highcharts-average-series' className: 'highcharts-average-series'
}); });
new Highcharts.Chart(chartDiv, { const chart = new Highcharts.Chart(chartDiv, {
credits: { credits: {
text: count === $params.hourly.length - 1 ? 'Open-Meteo.com' : '', text: count === ($params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '',
href: 'http://open-meteo.com' href: 'http://open-meteo.com'
}, },
@@ -183,8 +194,8 @@
subtitle: { subtitle: {
text: text:
count === 0 count === 0
? `Compare <span class="font-bold">${$params.hourly.join(', ')}</span> in models: <span class="font-bold">` + ? `Compare <span class="font-bold">${$params.hourly?.join(', ')}</span> in models: <span class="font-bold">` +
$params.models.join(', ') + $params.models?.join(', ') +
'</span>' '</span>'
: '', : '',
align: 'left' align: 'left'
@@ -251,20 +262,24 @@
}); });
count++; count++;
charts.push(chart);
// eslint-disable-next-line svelte/no-dom-manipulating
node.appendChild(chartDiv); node.appendChild(chartDiv);
} }
})();
} }
}); });
onDestroy(() => { onDestroy(() => {
if (chart) { charts.forEach((c) => c.destroy());
chart.destroy();
}
}); });
</script> </script>
<!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] --> <!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] -->
<div class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * $params.hourly.length + 2}px]"> <div
class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * ($params.hourly?.length || 0) +
2}px]"
>
<div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div> <div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div>
<div <div
class="{count > 0 class="{count > 0
@@ -319,12 +334,12 @@
<div class="mt-4 md:mt-8"> <div class="mt-4 md:mt-8">
<div class="flex"> <div class="flex">
<a href="#models"><h2 id="models" class="text-2xl md:text-3xl">Models</h2></a> <a href="#models"><h2 id="models" class="text-2xl md:text-3xl">Models</h2></a>
{#if $params.models.length > 0} {#if $params.models && $params.models.length > 0}
<div transition:fade={{ duration: 200 }} class="relative mt-[5px]"> <div transition:fade={{ duration: 200 }} class="relative mt-[5px]">
<div <div
class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline" class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
> >
{$params.models.length}&nbsp;/&nbsp;{models.flat().length} {$params.models?.length}&nbsp;/&nbsp;{models.flat().length}
</div> </div>
</div> </div>
{/if} {/if}
@@ -343,7 +358,7 @@
aria-labelledby="{value}_label" aria-labelledby="{value}_label"
onCheckedChange={() => { onCheckedChange={() => {
if ($params.models?.includes(value)) { if ($params.models?.includes(value)) {
$params.models = $params.models.filter((item) => { $params.models = $params.models.filter((item: string) => {
return item !== value; return item !== value;
}); });
} else if ($params.models) { } else if ($params.models) {
@@ -371,12 +386,12 @@
Hourly Weather Variables Hourly Weather Variables
</h2></a </h2></a
> >
{#if $params.hourly.length > 0} {#if $params.hourly && $params.hourly.length > 0}
<div transition:fade={{ duration: 200 }} class="relative mt-[5px]"> <div transition:fade={{ duration: 200 }} class="relative mt-[5px]">
<div <div
class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline" class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
> >
{$params.hourly.length}&nbsp;/&nbsp;{hourly.flat().length} {$params.hourly?.length}&nbsp;/&nbsp;{hourly.flat().length}
</div> </div>
</div> </div>
{/if} {/if}
@@ -397,7 +412,7 @@
aria-labelledby="{value}_label" aria-labelledby="{value}_label"
onCheckedChange={() => { onCheckedChange={() => {
if ($params.hourly?.includes(value)) { if ($params.hourly?.includes(value)) {
$params.hourly = $params.hourly.filter((item) => { $params.hourly = $params.hourly.filter((item: string) => {
return item !== value; return item !== value;
}); });
} else if ($params.hourly) { } else if ($params.hourly) {
+3 -1
View File
@@ -6,7 +6,8 @@ export const defaultParameters = {
}; };
export const models = [ export const models = [
{ value: 'best_match', label: 'Best match' }, [{ value: 'best_match', label: 'Best match' }],
[
{ value: 'gfs_seamless', label: 'NCEP GFS Seamless' }, { value: 'gfs_seamless', label: 'NCEP GFS Seamless' },
{ value: 'jma_seamless', label: 'JMA Seamless' }, { value: 'jma_seamless', label: 'JMA Seamless' },
{ value: 'kma_seamless', label: 'KMA Seamless' }, { value: 'kma_seamless', label: 'KMA Seamless' },
@@ -18,6 +19,7 @@ export const models = [
{ value: 'knmi_seamless', label: 'KNMI Seamless (with ECMWF)' }, { value: 'knmi_seamless', label: 'KNMI Seamless (with ECMWF)' },
{ value: 'dmi_seamless', label: 'DMI Seamless (with ECMWF)' }, { value: 'dmi_seamless', label: 'DMI Seamless (with ECMWF)' },
{ value: 'ukmo_seamless', label: 'UK Met Office Seamless' } { value: 'ukmo_seamless', label: 'UK Met Office Seamless' }
]
]; ];
export const hourly = [ export const hourly = [
+4 -4
View File
@@ -2,13 +2,13 @@ import { get } from 'svelte/store';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { storedLocation } from '$lib/stores/settings'; import { storedLocation } from '$lib/stores/settings';
import { geoLocationNameToRoute } from '$lib/utils/meteo'; import { geoLocationNameToRoute } from '$lib/utils/meteo';
import type { PageLoad } from '$types'; import type { PageLoad } from './$types';
export const prerender = true; export const prerender = true;
export const load = (async () => { export const load: PageLoad = async () => {
const location = get(storedLocation); const location = get(storedLocation);
const locationRoute = geoLocationNameToRoute(location.name); const locationRoute = geoLocationNameToRoute(location.name || '');
throw redirect( throw redirect(
303, 303,
'/weather/week/' + '/weather/week/' +
@@ -18,4 +18,4 @@ export const load = (async () => {
: locationRoute + '_' + location.id : locationRoute + '_' + location.id
: locationRoute + '_' + location.id) : locationRoute + '_' + location.id)
); );
}) satisfies PageLoad; };
@@ -21,7 +21,7 @@
import { getColor, textWhite } from '../../utils/colors'; import { getColor, textWhite } from '../../utils/colors';
import weatherCodes from '../../utils/weather-codes'; import weatherCodes from '../../utils/weather-codes';
import { SvelteDate } from 'svelte/reactivity'; import { SvelteDate } from 'svelte/reactivity';
import { pad } from '$lib/utils'; import { pad } from '$lib/utils/index.js';
// --- Interfaces --- // --- Interfaces ---
@@ -63,9 +63,9 @@
// --- State Setup --- // --- State Setup ---
const params = urlHashStore({ const params = urlHashStore({
latitude: [$storedLocation.latitude], latitude: [$storedLocation.latitude || 0],
longitude: [$storedLocation.longitude], longitude: [$storedLocation.longitude || 0],
models: 'best_match', models: ['best_match'],
...defaultParameters ...defaultParameters
}); });
@@ -89,7 +89,9 @@
const entries = 6; const entries = 6;
const winddir = true; const winddir = true;
let modelSelected = $derived(models.find((mo) => String(mo.value) === $params.models)); let modelSelected = $derived(
models.flat().find((mo) => $params.models?.includes(String(mo.value)))
);
// --- Logic --- // --- Logic ---
@@ -654,13 +656,20 @@
<div> <div>
<div class="mt-6 flex gap-6 md:mt-12"> <div class="mt-6 flex gap-6 md:mt-12">
<div class="relative w-1/2"> <div class="relative w-1/2">
<Select.Root name="model_selection" type="single" bind:value={$params.models}> <Select.Root
name="model_selection"
type="single"
value={$params.models?.[0]}
onValueChange={(v) => {
$params.models = [v];
}}
>
<Select.Trigger <Select.Trigger
aria-label="Forecast days input" aria-label="Forecast days input"
class="h-12 cursor-pointer pt-6 [&_svg]:mb-3">{modelSelected?.label}</Select.Trigger class="h-12 cursor-pointer pt-6 [&_svg]:mb-3">{modelSelected?.label}</Select.Trigger
> >
<Select.Content preventScroll={false} class="border-border"> <Select.Content preventScroll={false} class="border-border">
{#each models as mo (mo.value)} {#each models.flat() as mo (mo.value)}
<Select.Item class="cursor-pointer" value={mo.value}>{mo.label}</Select.Item> <Select.Item class="cursor-pointer" value={mo.value}>{mo.label}</Select.Item>
{/each} {/each}
</Select.Content> </Select.Content>
+6 -6
View File
@@ -4,11 +4,11 @@ import { type GeoLocation, storedLocation } from '$lib/stores/settings';
import { geoLocationNameToRoute } from '$lib/utils/meteo'; import { geoLocationNameToRoute } from '$lib/utils/meteo';
import type { PageLoad } from '$types'; import type { PageLoad } from './$types';
export const prerender = true; export const prerender = true;
export const load = (async (event) => { export const load: PageLoad = async (event) => {
const urlLocation = event.params.location; const urlLocation = event.params.location;
let urlLocationSplit, urlLocationName, urlLocationId; let urlLocationSplit, urlLocationName, urlLocationId;
@@ -31,8 +31,8 @@ export const load = (async (event) => {
// lat, long coordinates // lat, long coordinates
if (urlLocation.includes('N') && urlLocation.includes('E')) { if (urlLocation.includes('N') && urlLocation.includes('E')) {
urlLocationSplit = urlLocation.split(/N|E/); urlLocationSplit = urlLocation.split(/N|E/);
const latitude = urlLocationSplit[0]; const latitude = Number(urlLocationSplit[0]);
const longitude = urlLocationSplit[1]; const longitude = Number(urlLocationSplit[1]);
location = { location = {
//id: undefined, //id: undefined,
@@ -58,7 +58,7 @@ export const load = (async (event) => {
} }
} }
const locationRoute = geoLocationNameToRoute(location.name); const locationRoute = geoLocationNameToRoute(location.name || '');
if (location.population && location.population > 543000) { if (location.population && location.population > 543000) {
// 1000 biggest cities // 1000 biggest cities
@@ -74,4 +74,4 @@ export const load = (async (event) => {
storedLocation.set(location); storedLocation.set(location);
return { location: location }; return { location: location };
}) satisfies PageLoad; };
+5 -1
View File
@@ -7,7 +7,11 @@ const config = {
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { adapter: adapter() } kit: {
adapter: adapter({
fallback: '404.html'
})
}
}; };
export default config; export default config;