diff --git a/package.json b/package.json index f6dff6e..ae75d41 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/lib/components/location/location-search.svelte b/src/lib/components/location/location-search.svelte index 0d0b98a..11e1934 100644 --- a/src/lib/components/location/location-search.svelte +++ b/src/lib/components/location/location-search.svelte @@ -1,12 +1,12 @@ - - + { - e.preventDefault(); - dialogOpen = !dialogOpen; - }} > {label} - + - - - - - - Find Location -

Search for a city or use GPS

-
- -
-
-
-
- -
- + { + e.preventDefault(); + focusInput(); + }} + > +
+
+
+
+
-
- -
- {#await results} -
-
-
- Searching... -
-
- {: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)} - - {/each} -
- {/if} - {:catch error} - - Error: {error.message} - - {/await} +
- - - - +
+ {#await results} +
+
+
+ Searching... +
+
+ {: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)} + + {/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 @@