-
-
-
-
-
(searchQuery = 'GPS')}
- >
-
-
-
-
-
+
{
+ e.preventDefault();
+ focusInput();
+ }}
+ >
+
+
+
-
-
- {#await results}
-
- {:then results}
- {#if results.results && results.results.length === 0}
- {#if searchQuery.length < 2}
-
-
-
-
-
- Start typing to search or use GPS to detect your position
-
-
- {:else}
-
-
- No locations found for "{searchQuery}". Try a different term.
-
-
- {/if}
- {:else if !results.results}
-
- No locations found
-
- {:else}
-
- {#each results.results || [] as location, i (i)}
-
selectLocation(location)}
- >
-
-
-
-
- {location.name}
-
-
- {location.admin1 || ''}
- {location.country || ''}
- · {location.latitude.toFixed(2)}°N {location.longitude.toFixed(2)}°E
- {#if location.elevation}· {location.elevation.toFixed(0)}m{/if}
-
-
-
-
-
-
-
- {/each}
-
- {/if}
- {:catch error}
-
- Error: {error.message}
-
- {/await}
+
(searchQuery = 'GPS')}
+ >
+
+
+
+
+
-
-
-
-
+
+ {#await results}
+
+ {:then results}
+ {#if results.results && results.results.length === 0}
+ {#if searchQuery.length < 2}
+
+
+
+
+
+ Start typing to search or use GPS to detect your position
+
+
+ {:else}
+
+
+ No locations found for "{searchQuery}". Try a different term.
+
+
+ {/if}
+ {:else if !results.results}
+
+ No locations found
+
+ {:else}
+
+ {#each results.results || [] as location, i (i)}
+
selectLocation(location)}
+ >
+
+
+
+
+ {location.name}
+
+
+ {location.admin1 || ''}
+ {location.country || ''}
+ · {location.latitude.toFixed(2)}°N {location.longitude.toFixed(2)}°E
+ {#if location.elevation}· {location.elevation.toFixed(0)}m{/if}
+
+
+
+
+
+
+
+ {/each}
+
+ {/if}
+ {:catch error}
+
+ Error: {error.message}
+
+ {/await}
+
+
+
+
diff --git a/src/lib/components/navigation/topbar.svelte b/src/lib/components/navigation/topbar.svelte
index c360998..9b106e3 100644
--- a/src/lib/components/navigation/topbar.svelte
+++ b/src/lib/components/navigation/topbar.svelte
@@ -1,7 +1,13 @@
{
- storedLocation.set(event.detail);
+ navigateToLocation(event.detail);
}}
/>
diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts
new file mode 100644
index 0000000..63e21bf
--- /dev/null
+++ b/src/lib/components/ui/popover/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/popover/popover-close.svelte b/src/lib/components/ui/popover/popover-close.svelte
new file mode 100644
index 0000000..dc4dec4
--- /dev/null
+++ b/src/lib/components/ui/popover/popover-close.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte
new file mode 100644
index 0000000..3f62623
--- /dev/null
+++ b/src/lib/components/ui/popover/popover-content.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/popover/popover-portal.svelte b/src/lib/components/ui/popover/popover-portal.svelte
new file mode 100644
index 0000000..25efb87
--- /dev/null
+++ b/src/lib/components/ui/popover/popover-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/lib/components/ui/popover/popover-trigger.svelte b/src/lib/components/ui/popover/popover-trigger.svelte
new file mode 100644
index 0000000..b3dbc2c
--- /dev/null
+++ b/src/lib/components/ui/popover/popover-trigger.svelte
@@ -0,0 +1,18 @@
+
+
+
diff --git a/src/lib/components/ui/popover/popover.svelte b/src/lib/components/ui/popover/popover.svelte
new file mode 100644
index 0000000..f39b867
--- /dev/null
+++ b/src/lib/components/ui/popover/popover.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/lib/utils/location.ts b/src/lib/utils/location.ts
new file mode 100644
index 0000000..4a2c3d7
--- /dev/null
+++ b/src/lib/utils/location.ts
@@ -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
{
+ 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;
+}
diff --git a/src/routes/weather/+layout.ts b/src/routes/weather/+layout.ts
index d2a2971..d12525b 100644
--- a/src/routes/weather/+layout.ts
+++ b/src/routes/weather/+layout.ts
@@ -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
diff --git a/src/routes/weather/14-day/+layout.ts b/src/routes/weather/14-day/+layout.ts
index 55c9dc1..4ab5514 100644
--- a/src/routes/weather/14-day/+layout.ts
+++ b/src/routes/weather/14-day/+layout.ts
@@ -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
diff --git a/src/routes/weather/14-day/+page.ts b/src/routes/weather/14-day/+page.ts
new file mode 100644
index 0000000..a860658
--- /dev/null
+++ b/src/routes/weather/14-day/+page.ts
@@ -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;
diff --git a/src/routes/weather/14-day/+page.svelte b/src/routes/weather/14-day/[location]/+page.svelte
similarity index 95%
rename from src/routes/weather/14-day/+page.svelte
rename to src/routes/weather/14-day/[location]/+page.svelte
index 1500798..94c2ea9 100644
--- a/src/routes/weather/14-day/+page.svelte
+++ b/src/routes/weather/14-day/[location]/+page.svelte
@@ -1,8 +1,7 @@