feat: move nav bar to the side #6
+1
-1
@@ -14,7 +14,7 @@
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test:unit": "vitest",
|
||||
"test": "npm run test:unit -- --run",
|
||||
"upgrade:ui": "npx shadcn-svelte@latest add alert button card checkbox dialog input label select separator switch -y -o && prettier --write src/lib/components/ui"
|
||||
"upgrade:ui": "npx shadcn-svelte@latest add alert button card checkbox dialog input label popover select separator switch -y -o && prettier --write src/lib/components/ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.0",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||
import { createEventDispatcher, onDestroy, tick } from 'svelte';
|
||||
|
||||
import { type GeoLocation } from '$lib/stores/settings';
|
||||
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
|
||||
export let label: string = 'Search location...';
|
||||
export let placeholder: string = 'Enter city name...';
|
||||
@@ -18,26 +18,32 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
let debounceTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
let searchQuery = '';
|
||||
let popoverOpen = false;
|
||||
let searchInputEl: HTMLInputElement | null = null;
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(debounceTimeout);
|
||||
clearTimeout(debounceTimeout);
|
||||
});
|
||||
|
||||
let scrollY: number | undefined;
|
||||
|
||||
const closeModal = () => {
|
||||
dialogOpen = false;
|
||||
if (scrollY) {
|
||||
window.scrollTo({ top: scrollY, behavior: 'instant' });
|
||||
}
|
||||
const closePopover = () => {
|
||||
popoverOpen = false;
|
||||
};
|
||||
|
||||
const selectLocation = (location: GeoLocation) => {
|
||||
searchQuery = '';
|
||||
closeModal();
|
||||
closePopover();
|
||||
dispatch('location', location);
|
||||
};
|
||||
|
||||
async function focusInput() {
|
||||
await tick();
|
||||
searchInputEl?.focus();
|
||||
}
|
||||
|
||||
$: if (popoverOpen) {
|
||||
focusInput();
|
||||
}
|
||||
|
||||
$: results = (async () => {
|
||||
if (debounceTimeout) {
|
||||
clearTimeout(debounceTimeout);
|
||||
@@ -91,17 +97,11 @@
|
||||
|
||||
return (await result.json()) as ResultSet;
|
||||
})();
|
||||
|
||||
let dialogOpen = false;
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={dialogOpen}>
|
||||
<Dialog.Trigger
|
||||
<Popover.Root bind:open={popoverOpen}>
|
||||
<Popover.Trigger
|
||||
class="flex h-9 w-full cursor-pointer items-center gap-2 rounded-md border border-border bg-background px-3 text-[0.8125rem] text-muted-foreground transition-[border-color,box-shadow] duration-150 hover:border-primary"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
dialogOpen = !dialogOpen;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="h-3.5 w-3.5 shrink-0 opacity-50"
|
||||
@@ -115,35 +115,37 @@
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{label}</span>
|
||||
</Dialog.Trigger>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="dialog-overlay" />
|
||||
|
||||
<Dialog.Content class="dialog-content">
|
||||
<Dialog.Header class="pb-4">
|
||||
<Dialog.Title class="text-lg font-semibold">Find Location</Dialog.Title>
|
||||
<p class="text-sm text-muted-foreground">Search for a city or use GPS</p>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="px-5">
|
||||
<div class="mb-4 flex gap-2">
|
||||
<Popover.Content
|
||||
class="popover-dropdown w-(--bits-popover-anchor-width) min-w-[320px] p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
onOpenAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
focusInput();
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="p-3">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<Input
|
||||
type="search"
|
||||
{placeholder}
|
||||
class="h-10"
|
||||
class="h-9"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-label="Search Location"
|
||||
bind:value={searchQuery}
|
||||
bind:ref={searchInputEl}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
class="px-3"
|
||||
class="h-9 px-2.5"
|
||||
title="Use GPS Location"
|
||||
onclick={() => (searchQuery = 'GPS')}
|
||||
>
|
||||
@@ -165,11 +167,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-5 pb-5">
|
||||
<div class="max-h-[min(400px,50vh)] overflow-y-auto px-3 pb-3">
|
||||
{#await results}
|
||||
<div class="flex h-24 items-center justify-center">
|
||||
<div class="flex h-20 items-center justify-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="h-5 w-5 animate-spin rounded-full border-b-2 border-primary"></div>
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-primary"></div>
|
||||
<span class="text-sm text-muted-foreground">Searching...</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,10 +179,10 @@
|
||||
{#if results.results && results.results.length === 0}
|
||||
{#if searchQuery.length < 2}
|
||||
<div
|
||||
class="flex items-start gap-2 rounded-md bg-primary/8 p-3 text-muted-foreground"
|
||||
class="flex items-start gap-2 rounded-md bg-primary/8 p-2.5 text-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="mt-0.5 h-3.5 w-3.5 shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -192,7 +194,7 @@
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm">
|
||||
<span class="text-xs">
|
||||
Start typing to search or use GPS to detect your position
|
||||
</span>
|
||||
</div>
|
||||
@@ -210,15 +212,15 @@
|
||||
<Alert.Description>No locations found</Alert.Description>
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-0.5">
|
||||
{#each results.results || [] as location, i (i)}
|
||||
<button
|
||||
class="group block w-full cursor-pointer rounded-md border border-transparent bg-transparent px-3 py-2.5 transition-[background,border-color] duration-150 hover:border-border hover:bg-accent"
|
||||
class="group block w-full cursor-pointer rounded-md border border-transparent bg-transparent px-2.5 py-2 transition-[background,border-color] duration-150 hover:border-border hover:bg-accent"
|
||||
onclick={() => selectLocation(location)}
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<img
|
||||
class="h-8 w-8 rounded-full"
|
||||
class="h-7 w-7 rounded-full"
|
||||
src="/images/country-flags/{(
|
||||
location.country_code || 'united_nations'
|
||||
).toLowerCase()}.svg"
|
||||
@@ -236,7 +238,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="h-4 w-4 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
||||
class="h-3.5 w-3.5 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -260,34 +262,5 @@
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
|
||||
<style>
|
||||
:global(.dialog-overlay) {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:global(.dialog-content) {
|
||||
position: fixed;
|
||||
top: 10% !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 10%);
|
||||
min-height: 400px;
|
||||
width: calc(100% - 2rem);
|
||||
max-width: 560px;
|
||||
overflow: hidden;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
import LocationSearch from '$lib/components/location/location-search.svelte';
|
||||
|
||||
@@ -16,6 +22,20 @@
|
||||
storedLocation.subscribe((value) => {
|
||||
location = value;
|
||||
});
|
||||
|
||||
function navigateToLocation(newLocation: GeoLocation) {
|
||||
storedLocation.set(newLocation);
|
||||
const locationRoute = buildLocationRoute(newLocation);
|
||||
const currentPath = get(page).url.pathname;
|
||||
|
||||
if (currentPath.startsWith('/weather/compare')) {
|
||||
goto(resolve('/weather/compare/[location]', { location: locationRoute }));
|
||||
} else if (currentPath.startsWith('/weather/14-day')) {
|
||||
goto(resolve('/weather/14-day/[location]', { location: locationRoute }));
|
||||
} else {
|
||||
goto(resolve('/weather/week/[location]', { location: locationRoute }));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
@@ -60,7 +80,7 @@
|
||||
<LocationSearch
|
||||
label="Search location..."
|
||||
on:location={(event) => {
|
||||
storedLocation.set(event.detail);
|
||||
navigateToLocation(event.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Close from './popover-close.svelte';
|
||||
import Content from './popover-content.svelte';
|
||||
import Portal from './popover-portal.svelte';
|
||||
import Trigger from './popover-trigger.svelte';
|
||||
import Root from './popover.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
Portal,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
Portal as PopoverPortal
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
import { type WithoutChildrenOrChild, cn } from '$lib/utils/ui.js';
|
||||
|
||||
import PopoverPortal from './popover-portal.svelte';
|
||||
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = 'center',
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPortal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPortal>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PopoverPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="popover-trigger"
|
||||
class={cn('', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Root bind:open {...restProps} />
|
||||
@@ -0,0 +1,103 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { geoLocationNameToRoute } from '$lib/utils/meteo';
|
||||
|
||||
export function buildLocationRoute(location: GeoLocation): string {
|
||||
const locationRoute = geoLocationNameToRoute(location.name);
|
||||
if (location.population && location.population > 543000) {
|
||||
return locationRoute;
|
||||
}
|
||||
return locationRoute + '_' + location.id;
|
||||
}
|
||||
|
||||
interface ResolveLocationOptions {
|
||||
urlLocation: string;
|
||||
routePrefix: string;
|
||||
event: {
|
||||
fetch: typeof fetch;
|
||||
url: URL;
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveLocationFromRoute({
|
||||
urlLocation,
|
||||
routePrefix,
|
||||
event
|
||||
}: ResolveLocationOptions): Promise<GeoLocation> {
|
||||
let location: GeoLocation;
|
||||
|
||||
if (urlLocation.includes('N') && urlLocation.includes('E')) {
|
||||
const parts = urlLocation.split(/N|E/);
|
||||
const latitude = parseFloat(parts[0]);
|
||||
const longitude = parseFloat(parts[1]);
|
||||
|
||||
location = {
|
||||
id: 0,
|
||||
name: `${latitude}N° ${longitude}E°`,
|
||||
latitude,
|
||||
longitude,
|
||||
elevation: 0,
|
||||
feature_code: 'COORD',
|
||||
country_code: undefined,
|
||||
admin1_id: undefined,
|
||||
admin3_id: undefined,
|
||||
admin4_id: undefined,
|
||||
timezone: 'UTC',
|
||||
population: undefined,
|
||||
postcodes: undefined,
|
||||
country_id: undefined,
|
||||
country: undefined,
|
||||
admin1: undefined,
|
||||
admin3: undefined,
|
||||
admin4: undefined
|
||||
};
|
||||
} else {
|
||||
let urlLocationName: string;
|
||||
let urlLocationId: string | undefined;
|
||||
|
||||
if (urlLocation.includes('_')) {
|
||||
const split = urlLocation.split('_');
|
||||
urlLocationName = split[0];
|
||||
urlLocationId = split[1];
|
||||
} else if (/^\d+$/.test(urlLocation)) {
|
||||
urlLocationName = '';
|
||||
urlLocationId = urlLocation;
|
||||
} else {
|
||||
urlLocationName = urlLocation;
|
||||
urlLocationId = undefined;
|
||||
}
|
||||
|
||||
if (urlLocationId) {
|
||||
const res = await event.fetch(
|
||||
`https://geocoding-api.open-meteo.com/v1/get?id=${urlLocationId}`
|
||||
);
|
||||
location = await res.json();
|
||||
} else {
|
||||
const res = await event.fetch(
|
||||
`https://geocoding-api.open-meteo.com/v1/search?name=${urlLocationName}&count=1&language=en&format=json`
|
||||
);
|
||||
const geocodingResponse = await res.json();
|
||||
if (geocodingResponse.results) {
|
||||
location = geocodingResponse.results[0];
|
||||
} else {
|
||||
error(404, 'Location not found');
|
||||
}
|
||||
}
|
||||
|
||||
const locationRoute = geoLocationNameToRoute(location.name);
|
||||
const canonicalSuffix =
|
||||
location.population && location.population > 543000
|
||||
? locationRoute
|
||||
: locationRoute + '_' + location.id;
|
||||
const canonicalPath = `${routePrefix}${canonicalSuffix}`;
|
||||
|
||||
if (event.url.pathname !== canonicalPath) {
|
||||
throw redirect(303, canonicalPath);
|
||||
}
|
||||
}
|
||||
|
||||
storedLocation.set(location);
|
||||
return location;
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
const location = get(storedLocation);
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
const location = get(storedLocation);
|
||||
return {
|
||||
title: `Weather ${location.name}`,
|
||||
location: location
|
||||
|
||||
@@ -4,9 +4,8 @@ import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
const location = get(storedLocation);
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
const location = get(storedLocation);
|
||||
return {
|
||||
heroTitle: `14 Day Weather ${location.name}`,
|
||||
heroDescription: location.admin1 ?? '' + ' ' + location.country
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
const location = get(storedLocation);
|
||||
const locationRoute = buildLocationRoute(location);
|
||||
throw redirect(303, '/weather/14-day/' + locationRoute);
|
||||
}) satisfies PageLoad;
|
||||
+10
-8
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import {
|
||||
buildAverageSeries,
|
||||
@@ -24,7 +23,7 @@
|
||||
fetchEnsembleForecast
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { defaultParameters } from '../options';
|
||||
import { defaultParameters } from '../../options';
|
||||
|
||||
import type * as echarts from 'echarts';
|
||||
|
||||
@@ -40,11 +39,12 @@
|
||||
let mounted = $state(false);
|
||||
let loading = $state(true);
|
||||
|
||||
const location = get(storedLocation);
|
||||
let location = $state<GeoLocation>($storedLocation);
|
||||
storedLocation.subscribe((value) => {
|
||||
location = value;
|
||||
});
|
||||
|
||||
let params = $state({
|
||||
latitude: [52.52],
|
||||
longitude: [13.41],
|
||||
...defaultParameters,
|
||||
hourly: ['temperature_2m'],
|
||||
models: ['gfs_seamless']
|
||||
@@ -87,14 +87,16 @@
|
||||
|
||||
if (!mounted || !hourlyVars?.length || !modelList?.length) return;
|
||||
|
||||
const loc = location;
|
||||
|
||||
const loadData = async () => {
|
||||
loading = true;
|
||||
chartInstances = [];
|
||||
chartComponents = [];
|
||||
|
||||
const result: EnsembleForecastResult = await fetchEnsembleForecast({
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
latitude: loc.latitude!,
|
||||
longitude: loc.longitude!,
|
||||
hourlyVariables: hourlyVars,
|
||||
models: modelList,
|
||||
forecast_days: 14,
|
||||
@@ -0,0 +1,13 @@
|
||||
import { resolveLocationFromRoute } from '$lib/utils/location';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
const location = await resolveLocationFromRoute({
|
||||
urlLocation: event.params.location,
|
||||
routePrefix: '/weather/14-day/',
|
||||
event
|
||||
});
|
||||
|
||||
return { location };
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
const location = get(storedLocation);
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
return {
|
||||
heroTitle: `Model Compare ${location.name}`,
|
||||
heroDescription: location.admin1 ?? '' + ' ' + location.country
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
const location = get(storedLocation);
|
||||
const locationRoute = buildLocationRoute(location);
|
||||
throw redirect(303, '/weather/compare/' + locationRoute);
|
||||
}) satisfies PageLoad;
|
||||
+11
-9
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import {
|
||||
buildAverageSeries,
|
||||
@@ -28,8 +27,8 @@
|
||||
fetchModelComparison
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { hourly, models as modelsFlat } from '../options';
|
||||
import { defaultParameters } from '../options';
|
||||
import { hourly, models as modelsFlat } from '../../options';
|
||||
import { defaultParameters } from '../../options';
|
||||
|
||||
import type * as echarts from 'echarts';
|
||||
|
||||
@@ -47,11 +46,12 @@
|
||||
let mounted = $state(false);
|
||||
let loading = $state(true);
|
||||
|
||||
const location = get(storedLocation);
|
||||
let location = $state<GeoLocation>($storedLocation);
|
||||
storedLocation.subscribe((value) => {
|
||||
location = value;
|
||||
});
|
||||
|
||||
let params = $state({
|
||||
latitude: [52.52],
|
||||
longitude: [13.41],
|
||||
...defaultParameters,
|
||||
hourly: ['temperature_2m', 'rain', 'relative_humidity_2m'],
|
||||
models: [
|
||||
@@ -101,14 +101,16 @@
|
||||
|
||||
if (!mounted || !hourlyVars?.length || !modelList?.length) return;
|
||||
|
||||
const loc = location;
|
||||
|
||||
const loadData = async () => {
|
||||
loading = true;
|
||||
chartInstances = [];
|
||||
chartComponents = [];
|
||||
|
||||
const result: ModelCompareResult = await fetchModelComparison({
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
latitude: loc.latitude!,
|
||||
longitude: loc.longitude!,
|
||||
hourlyVariables: hourlyVars,
|
||||
models: modelList,
|
||||
temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit',
|
||||
@@ -0,0 +1,13 @@
|
||||
import { resolveLocationFromRoute } from '$lib/utils/location';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
const location = await resolveLocationFromRoute({
|
||||
urlLocation: event.params.location,
|
||||
routePrefix: '/weather/compare/',
|
||||
event
|
||||
});
|
||||
|
||||
return { location };
|
||||
};
|
||||
@@ -4,20 +4,12 @@ import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { geoLocationNameToRoute } from '$lib/utils/meteo';
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
const location = get(storedLocation);
|
||||
const locationRoute = geoLocationNameToRoute(location.name);
|
||||
throw redirect(
|
||||
303,
|
||||
'/weather/week/' +
|
||||
(location.population
|
||||
? location.population > 543000
|
||||
? locationRoute
|
||||
: locationRoute + '_' + location.id
|
||||
: locationRoute + '_' + location.id)
|
||||
);
|
||||
const locationRoute = buildLocationRoute(location);
|
||||
throw redirect(303, '/weather/week/' + locationRoute);
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -1,89 +1,13 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { geoLocationNameToRoute } from '$lib/utils/meteo';
|
||||
import { resolveLocationFromRoute } from '$lib/utils/location';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
const urlLocation = event.params.location;
|
||||
let urlLocationSplit, urlLocationName, urlLocationId;
|
||||
const location = await resolveLocationFromRoute({
|
||||
urlLocation: event.params.location,
|
||||
routePrefix: '/weather/week/',
|
||||
event
|
||||
});
|
||||
|
||||
if (urlLocation.includes('_')) {
|
||||
urlLocationSplit = urlLocation.split('_');
|
||||
urlLocationName = urlLocationSplit[0];
|
||||
urlLocationId = urlLocationSplit[1];
|
||||
} else if (/^\d+$/.test(urlLocation)) {
|
||||
// only numbers in location, must be geonames id
|
||||
urlLocationName = '';
|
||||
urlLocationId = urlLocation;
|
||||
} else if (/^[a-zA-Z]/.test(urlLocation)) {
|
||||
// only letters in location, must be geonames query
|
||||
urlLocationName = urlLocation;
|
||||
urlLocationId = undefined;
|
||||
}
|
||||
|
||||
let location: GeoLocation;
|
||||
|
||||
// lat, long coordinates
|
||||
if (urlLocation.includes('N') && urlLocation.includes('E')) {
|
||||
urlLocationSplit = urlLocation.split(/N|E/);
|
||||
const latitude = parseFloat(urlLocationSplit[0]);
|
||||
const longitude = parseFloat(urlLocationSplit[1]);
|
||||
|
||||
location = {
|
||||
id: 0,
|
||||
name: `${latitude}N° ${longitude}E°`,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
elevation: 0,
|
||||
feature_code: 'COORD',
|
||||
country_code: undefined,
|
||||
admin1_id: undefined,
|
||||
admin3_id: undefined,
|
||||
admin4_id: undefined,
|
||||
timezone: 'UTC',
|
||||
population: undefined,
|
||||
postcodes: undefined,
|
||||
country_id: undefined,
|
||||
country: undefined,
|
||||
admin1: undefined,
|
||||
admin3: undefined,
|
||||
admin4: undefined
|
||||
};
|
||||
} else {
|
||||
if (urlLocationId) {
|
||||
const res = await event.fetch(
|
||||
`https://geocoding-api.open-meteo.com/v1/get?id=${urlLocationId}`
|
||||
);
|
||||
location = await res.json();
|
||||
} else {
|
||||
const res = await event.fetch(
|
||||
`https://geocoding-api.open-meteo.com/v1/search?name=${urlLocationName}&count=1&language=en&format=json`
|
||||
);
|
||||
const geocodingResponse = await res.json();
|
||||
if (geocodingResponse.results) {
|
||||
location = geocodingResponse.results[0];
|
||||
} else {
|
||||
error(404, 'Location not found');
|
||||
}
|
||||
}
|
||||
|
||||
const locationRoute = geoLocationNameToRoute(location.name);
|
||||
|
||||
if (location.population && location.population > 543000) {
|
||||
// 1000 biggest cities
|
||||
if (event.url.pathname !== `/weather/week/${locationRoute}`) {
|
||||
throw redirect(303, `/weather/week/${locationRoute}`);
|
||||
}
|
||||
} else {
|
||||
if (event.url.pathname !== `/weather/week/${locationRoute + '_' + location.id}`) {
|
||||
throw redirect(303, `/weather/week/${locationRoute + '_' + location.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storedLocation.set(location);
|
||||
return { location: location };
|
||||
return { location };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user