From 852ae21a820e44d3b26c037c3defc70c78bb215f Mon Sep 17 00:00:00 2001 From: terraputix Date: Mon, 16 Feb 2026 00:52:18 +0100 Subject: [PATCH 1/2] move nav bar to the side and remove landing page --- .../location/location-search.svelte | 158 +++++----- src/lib/components/navigation/topbar.svelte | 73 +++++ .../components/navigation/weather-nav.svelte | 267 ++++++++++------- src/routes/+layout.svelte | 51 +++- src/routes/+page.svelte | 269 +----------------- src/routes/+page.ts | 7 + src/routes/layout.css | 116 ++++---- src/routes/page.svelte.spec.ts | 7 +- src/routes/weather/+layout.svelte | 209 +------------- .../weather/week/[location]/+page.svelte | 23 +- 10 files changed, 444 insertions(+), 736 deletions(-) create mode 100644 src/lib/components/navigation/topbar.svelte create mode 100644 src/routes/+page.ts diff --git a/src/lib/components/location/location-search.svelte b/src/lib/components/location/location-search.svelte index c26032c..0d0b98a 100644 --- a/src/lib/components/location/location-search.svelte +++ b/src/lib/components/location/location-search.svelte @@ -8,7 +8,7 @@ import * as Dialog from '$lib/components/ui/dialog'; import { Input } from '$lib/components/ui/input'; - export let label: string = 'Search Locations...'; + export let label: string = 'Search location...'; export let placeholder: string = 'Enter city name...'; interface ResultSet { @@ -97,50 +97,43 @@ { e.preventDefault(); dialogOpen = !dialogOpen; }} > - - {label} - + {label} - + - - - Find Your Location -

- Search for a city or use GPS to detect your location -

+ + + Find Location +

Search for a city or use GPS

-
-
+
+
-
+
{#await results} -
-
-
- Searching... +
+
+
+ Searching...
{:then results} {#if results.results && results.results.length === 0} {#if searchQuery.length < 2} - - +
+ - - Start typing to search for locations or use GPS to detect your current position - - + + Start typing to search or use GPS to detect your position + +
{:else} - - - - + - No locations found for "{searchQuery}". Try a different search term. + No locations found for "{searchQuery}". Try a different term. {/if} @@ -216,39 +210,33 @@ No locations found {:else} -
+
{#each results.results || [] as location, i (i)} - {/each} -
- - -
- -
-
- - - {#if mobileNavOpened} -
-
- {#each links as link (link.title)} - - {/each} -
-
+ + + {/if}
- + + + + + +
+ +
+ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1ff71bb..331afe5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,27 @@ @@ -12,6 +30,33 @@ -
- {@render children()} -
+
+ + + + + {#if mobileMenuOpen} + + {/if} + + +
+ + +
+ {@render children()} +
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index af09426..33e9f0e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,270 +1,3 @@ - - - Open-Meteo Weather - Advanced Weather Forecasting - + Open-Meteo Weather - -
- -
- {#if mounted} -
-

- Open-Meteo Weather -

-

- Professional weather forecasting with advanced models, detailed comparisons, and extended - predictions -

- - -
- -
-
- {/if} - - -
- {#each features as feature, index (feature.title)} - {#if mounted} -
- - -
- {#if feature.icon === 'calendar'} - - - - {:else if feature.icon === 'trending-up'} - - - - {:else if feature.icon === 'cloud'} - - - - {/if} -
- {feature.title} - - {feature.description} - -
- - - -
-
- {/if} - {/each} -
- - - {#if mounted && location} -
- - - - Current Location: {location.name} - - - {location.country} • {location.latitude.toFixed(2)}°, {location.longitude.toFixed(2)}° - - - - - - -
- {/if} -
- - -
-
- {#if mounted} -
-

- Why Choose Our Weather Service? -

-

- Powered by Open-Meteo API with multiple weather models and advanced visualization -

-
- -
- {#each [{ title: 'Multiple Models', desc: 'Compare ECMWF, GFS, and more', icon: 'layers' }, { title: 'Extended Forecasts', desc: 'Up to 14 days ahead', icon: 'clock' }, { title: 'Interactive Charts', desc: 'Apache ECharts visualization', icon: 'bar-chart' }, { title: 'Real-time Data', desc: 'Always up-to-date', icon: 'refresh' }] as feature, index (feature.title)} -
-
- - {#if feature.icon === 'layers'} - - {:else if feature.icon === 'clock'} - - {:else if feature.icon === 'bar-chart'} - - {:else if feature.icon === 'refresh'} - - {/if} - -
-

- {feature.title} -

-

{feature.desc}

-
- {/each} -
- {/if} -
-
-
- - diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..db2ed9b --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; + +import type { PageLoad } from './$types'; + +export const load = (async () => { + throw redirect(303, '/weather/week/'); +}) satisfies PageLoad; diff --git a/src/routes/layout.css b/src/routes/layout.css index b37c9b3..33a587d 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -6,71 +6,75 @@ :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.129 0.042 264.695); + --background: oklch(0.985 0.002 90); + --foreground: oklch(0.205 0.02 60); --card: oklch(1 0 0); - --card-foreground: oklch(0.129 0.042 264.695); + --card-foreground: oklch(0.205 0.02 60); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.129 0.042 264.695); - --primary: oklch(0.208 0.042 265.755); - --primary-foreground: oklch(0.984 0.003 247.858); - --secondary: oklch(0.968 0.007 247.896); - --secondary-foreground: oklch(0.208 0.042 265.755); - --muted: oklch(0.968 0.007 247.896); - --muted-foreground: oklch(0.554 0.046 257.417); - --accent: oklch(0.968 0.007 247.896); - --accent-foreground: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.205 0.02 60); + --primary: oklch(0.65 0.17 55); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.965 0.01 85); + --secondary-foreground: oklch(0.25 0.02 60); + --muted: oklch(0.96 0.008 85); + --muted-foreground: oklch(0.5 0.02 60); + --accent: oklch(0.96 0.008 85); + --accent-foreground: oklch(1 0 0); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.929 0.013 255.508); - --input: oklch(0.929 0.013 255.508); - --ring: oklch(0.704 0.04 256.788); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.984 0.003 247.858); - --sidebar-foreground: oklch(0.129 0.042 264.695); - --sidebar-primary: oklch(0.208 0.042 265.755); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.968 0.007 247.896); - --sidebar-accent-foreground: oklch(0.208 0.042 265.755); - --sidebar-border: oklch(0.929 0.013 255.508); - --sidebar-ring: oklch(0.704 0.04 256.788); + --border: oklch(0.91 0.01 80); + --input: oklch(0.91 0.01 80); + --ring: oklch(0.65 0.17 55); + --chart-1: oklch(0.65 0.17 55); + --chart-2: oklch(0.62 0.16 250); + --chart-3: oklch(0.75 0.15 75); + --chart-4: oklch(0.55 0.12 250); + --chart-5: oklch(0.8 0.14 65); + --sidebar-foreground: oklch(0.3 0.02 60); + --sidebar-primary: oklch(0.65 0.17 55); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.95 0.03 70); + --sidebar-accent-foreground: oklch(0.3 0.02 60); + --sidebar-border: oklch(0.92 0.01 80); + --sidebar-ring: oklch(0.65 0.17 55); + --sidebar: oklch(0.99 0.003 85); + --topbar-bg: oklch(1 0 0); + --topbar-border: oklch(0.92 0.01 80); } .dark { - --background: oklch(0.129 0.042 264.695); - --foreground: oklch(0.984 0.003 247.858); - --card: oklch(0.208 0.042 265.755); - --card-foreground: oklch(0.984 0.003 247.858); - --popover: oklch(0.208 0.042 265.755); - --popover-foreground: oklch(0.984 0.003 247.858); - --primary: oklch(0.929 0.013 255.508); - --primary-foreground: oklch(0.208 0.042 265.755); - --secondary: oklch(0.279 0.041 260.031); - --secondary-foreground: oklch(0.984 0.003 247.858); - --muted: oklch(0.279 0.041 260.031); - --muted-foreground: oklch(0.704 0.04 256.788); - --accent: oklch(0.279 0.041 260.031); - --accent-foreground: oklch(0.984 0.003 247.858); + --background: oklch(0.17 0.015 60); + --foreground: oklch(0.96 0.005 85); + --card: oklch(0.22 0.015 60); + --card-foreground: oklch(0.96 0.005 85); + --popover: oklch(0.22 0.015 60); + --popover-foreground: oklch(0.96 0.005 85); + --primary: oklch(0.72 0.17 55); + --primary-foreground: oklch(0.15 0.02 60); + --secondary: oklch(0.26 0.015 60); + --secondary-foreground: oklch(0.96 0.005 85); + --muted: oklch(0.26 0.015 60); + --muted-foreground: oklch(0.65 0.02 60); + --accent: oklch(0.65 0.16 250); + --accent-foreground: oklch(0.96 0.005 85); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.551 0.027 264.364); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.208 0.042 265.755); - --sidebar-foreground: oklch(0.984 0.003 247.858); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.279 0.041 260.031); - --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --ring: oklch(0.72 0.17 55); + --chart-1: oklch(0.72 0.17 55); + --chart-2: oklch(0.65 0.16 250); + --chart-3: oklch(0.8 0.14 65); + --chart-4: oklch(0.6 0.2 300); + --chart-5: oklch(0.7 0.22 20); + --sidebar-foreground: oklch(0.96 0.005 85); + --sidebar-primary: oklch(0.72 0.17 55); + --sidebar-primary-foreground: oklch(0.15 0.02 60); + --sidebar-accent: oklch(0.26 0.02 55); + --sidebar-accent-foreground: oklch(0.96 0.005 85); --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.551 0.027 264.364); + --sidebar-ring: oklch(0.72 0.17 55); + --sidebar: oklch(0.19 0.015 60); + --topbar-bg: oklch(0.2 0.015 60); + --topbar-border: oklch(1 0 0 / 10%); } @theme inline { @@ -109,6 +113,8 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --color-topbar: var(--topbar-bg); + --color-topbar-border: var(--topbar-border); } @layer base { diff --git a/src/routes/page.svelte.spec.ts b/src/routes/page.svelte.spec.ts index 5b110b3..08ddddc 100644 --- a/src/routes/page.svelte.spec.ts +++ b/src/routes/page.svelte.spec.ts @@ -1,14 +1,13 @@ import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; -import { page } from 'vitest/browser'; import Page from './+page.svelte'; describe('/+page.svelte', () => { - it('should render h1', async () => { + it('should render the redirect page with correct title', async () => { render(Page); - const heading = page.getByRole('heading', { level: 1 }); - await expect.element(heading).toBeInTheDocument(); + const title = document.querySelector('title'); + expect(title?.textContent).toBe('Open-Meteo Weather'); }); }); diff --git a/src/routes/weather/+layout.svelte b/src/routes/weather/+layout.svelte index c6723cb..8e8c880 100644 --- a/src/routes/weather/+layout.svelte +++ b/src/routes/weather/+layout.svelte @@ -1,216 +1,9 @@ - - -
- -
-
-
-
- {#if mounted} -
- -
-

- {getPageTitle()} -

-
- - -
-
- - {#if location} -
- -
-
- {location.country} -
-
-

- {location.name} -

-

- {#if location.admin1}{location.admin1}, - {/if}{location.country} -

-

- {location.latitude.toFixed(2)}°N, {location.longitude.toFixed(2)}°E - {#if location.elevation}• {location.elevation.toFixed(0)}m{/if} -

-
-
- - - {#if currentWeather} -
-
-
- {getWeatherIcon(currentWeather.current.weather_code)} -
-
- {Math.round(currentWeather.current.temperature_2m)}°C -
-
-
- {/if} -
- {/if} - - -
- { - storedLocation.set(event.detail); - }} - /> -
-
-
- - - {#if location?.population} -
-
-
Population
-
{location.population.toLocaleString()}
-
- {#if location.timezone} -
-
Timezone
-
{location.timezone}
-
- {/if} -
- {/if} -
- {/if} -
-
- - -
-
-
-
-
-
- - -
- {#if mounted} -
- {@render children?.()} -
- {/if} -
-
- - +{@render children?.()} diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte index f43e8cd..73f0c26 100644 --- a/src/routes/weather/week/[location]/+page.svelte +++ b/src/routes/weather/week/[location]/+page.svelte @@ -1,5 +1,5 @@ - - + { - 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 @@