From 945ebef7e18cd110f872bb1c1d1055ce87873200 Mon Sep 17 00:00:00 2001 From: Traines Date: Sun, 19 Jan 2025 01:41:19 +0100 Subject: [PATCH] (Mobile) UI improvements (#705) * ui: move components to lib * ui: make components more responsive * ui: more compact layout * ui: i18n fixes * ui: make toggle button state more visible * ui: error handling, avoid repeated effect triggering * ui: more small screen adjustments * ui: url state handling * ui: show backend error msgs, compact transfer display --- ui/package-lock.json | 46 ++--- ui/package.json | 2 +- ui/src/app.css | 8 + ui/src/app.d.ts | 10 +- .../{routes => lib}/ConnectionDetail.svelte | 59 ++++-- ui/src/lib/DateInput.svelte | 2 +- ui/src/{routes => lib}/Debug.svelte | 0 ui/src/lib/ErrorMessage.svelte | 9 + .../{routes => lib}/ItineraryGeoJSON.svelte | 0 ui/src/{routes => lib}/ItineraryList.svelte | 69 +++++-- ui/src/{routes => lib}/LevelSelect.svelte | 0 ui/src/{routes => lib}/RailViz.svelte | 15 +- ui/src/{routes => lib}/SearchMask.svelte | 22 ++- ui/src/{routes => lib}/StopTimes.svelte | 72 +++---- ui/src/lib/Time.svelte | 2 +- ui/src/lib/components/ui/card/card.svelte | 2 +- .../ui/radio-group/radio-group.svelte | 2 +- ui/src/lib/components/ui/toggle/toggle.svelte | 2 +- ui/src/lib/i18n/de.ts | 3 +- ui/src/lib/i18n/en.ts | 5 +- ui/src/lib/i18n/fr.ts | 13 +- ui/src/lib/i18n/pl.ts | 3 +- ui/src/lib/i18n/translation.ts | 1 + ui/src/lib/map/Control.svelte | 2 +- ui/src/lib/map/Map.svelte | 3 +- ui/src/routes/+page.svelte | 181 ++++++++++++------ 26 files changed, 328 insertions(+), 205 deletions(-) rename ui/src/{routes => lib}/ConnectionDetail.svelte (75%) rename ui/src/{routes => lib}/Debug.svelte (100%) create mode 100644 ui/src/lib/ErrorMessage.svelte rename ui/src/{routes => lib}/ItineraryGeoJSON.svelte (100%) rename ui/src/{routes => lib}/ItineraryList.svelte (72%) rename ui/src/{routes => lib}/LevelSelect.svelte (100%) rename ui/src/{routes => lib}/RailViz.svelte (95%) rename ui/src/{routes => lib}/SearchMask.svelte (84%) rename ui/src/{routes => lib}/StopTimes.svelte (57%) diff --git a/ui/package-lock.json b/ui/package-lock.json index 2e219b30e..d107c7bcf 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -24,7 +24,7 @@ "@hey-api/openapi-ts": "^0.53.11", "@playwright/test": "^1.28.1", "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/kit": "^2.7.2", + "@sveltejs/kit": "^2.12.0", "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@types/eslint": "^8.56.0", "@types/polyline": "^0.1.32", @@ -1357,25 +1357,23 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.1.tgz", - "integrity": "sha512-uuOfFwZ4xvnfPsiTB6a4H1ljjTUksGhWnYq5X/Y9z4x5+3uM2Md8q/YVeHL+7w+mygAwoEFdgKZ8YkUuk+VKww==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.16.0.tgz", + "integrity": "sha512-S9i1ZWKqluzoaJ6riYnEdbe+xJluMTMkhABouBa66GaWcAyCjW/jAc0NdJQJ/DXyK1CnP5quBW25e99MNyvLxA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "devalue": "^5.1.0", - "esm-env": "^1.0.0", + "esm-env": "^1.2.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0", - "tiny-glob": "^0.2.9" + "sirv": "^3.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" @@ -1384,9 +1382,9 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" + "vite": "^5.0.3 || ^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte": { @@ -2794,9 +2792,9 @@ } }, "node_modules/esm-env": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz", - "integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "license": "MIT" }, "node_modules/espree": { @@ -3270,12 +3268,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -3296,12 +3288,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5716,16 +5702,6 @@ "node": ">=0.8" } }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/ui/package.json b/ui/package.json index efa3d970f..c75621b5e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,7 +18,7 @@ "@hey-api/openapi-ts": "^0.53.11", "@playwright/test": "^1.28.1", "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/kit": "^2.7.2", + "@sveltejs/kit": "^2.12.0", "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@types/eslint": "^8.56.0", "@types/polyline": "^0.1.32", diff --git a/ui/src/app.css b/ui/src/app.css index 9580eb195..bd8cce8fe 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -97,3 +97,11 @@ .maplibregl-popup-content { background-color: hsl(var(--background)) !important; } + +.maplibregl-control-container .maplibregl-ctrl-top-left { + max-width: 100%; + bottom: 1rem; + display: flex; + flex-direction: column; + z-index: 3; +} diff --git a/ui/src/app.d.ts b/ui/src/app.d.ts index 743f07b2e..e51b83ab1 100644 --- a/ui/src/app.d.ts +++ b/ui/src/app.d.ts @@ -1,11 +1,19 @@ // See https://kit.svelte.dev/docs/types#app + +import type { Itinerary } from '$lib/openapi'; + // for information about these interfaces declare global { namespace App { // interface Error {} // interface Locals {} // interface PageData {} - // interface PageState {} + interface PageState { + selectedItinerary?: Itinerary; + selectedStop?: { name: string; stopId: string; time: Date }; + stopArriveBy?: boolean; + tripId?: string; + } // interface Platform {} } } diff --git a/ui/src/routes/ConnectionDetail.svelte b/ui/src/lib/ConnectionDetail.svelte similarity index 75% rename from ui/src/routes/ConnectionDetail.svelte rename to ui/src/lib/ConnectionDetail.svelte index e6386fd8e..767cd6450 100644 --- a/ui/src/routes/ConnectionDetail.svelte +++ b/ui/src/lib/ConnectionDetail.svelte @@ -1,7 +1,7 @@ + +
{e}
diff --git a/ui/src/routes/ItineraryGeoJSON.svelte b/ui/src/lib/ItineraryGeoJSON.svelte similarity index 100% rename from ui/src/routes/ItineraryGeoJSON.svelte rename to ui/src/lib/ItineraryGeoJSON.svelte diff --git a/ui/src/routes/ItineraryList.svelte b/ui/src/lib/ItineraryList.svelte similarity index 72% rename from ui/src/routes/ItineraryList.svelte rename to ui/src/lib/ItineraryList.svelte index 85f9d836a..fa9e40853 100644 --- a/ui/src/routes/ItineraryList.svelte +++ b/ui/src/lib/ItineraryList.svelte @@ -1,25 +1,44 @@ {#snippet legSummary(l: Leg)} @@ -50,7 +69,7 @@ { - selectedItinerary = d; + selectItinerary(d); }} /> {/each} @@ -58,7 +77,7 @@ {/if} {#if r.itineraries.length !== 0} -
+
{#each routingResponses as r, rI} {#await r}
@@ -73,12 +92,14 @@ routingResponses.splice( 0, 0, - plan({ - query: { ...baseQuery.query, pageCursor: r.previousPageCursor } - }).then((x) => x.data!) + throwOnError( + plan({ + query: { ...baseQuery.query, pageCursor: r.previousPageCursor } + }) + ) ); }} - class="px-2 py-1 bg-blue-600 hover:!bg-blue-700 text-white font-bold text-sm border rounded-lg" + class="px-2 py-1 bg-blue-600 hover:!bg-blue-700 text-white font-bold text-sm border rounded-lg text-nowrap" > {t.earlier} @@ -88,12 +109,12 @@ {#each r.itineraries as it} @@ -153,10 +176,14 @@
{/if} {:catch e} -
Error: {e}
+ {/await} {/each}
+ {:else if r.direct.length === 0} + {/if} + {:catch e} + {/await} {/if} diff --git a/ui/src/routes/LevelSelect.svelte b/ui/src/lib/LevelSelect.svelte similarity index 100% rename from ui/src/routes/LevelSelect.svelte rename to ui/src/lib/LevelSelect.svelte diff --git a/ui/src/routes/RailViz.svelte b/ui/src/lib/RailViz.svelte similarity index 95% rename from ui/src/routes/RailViz.svelte rename to ui/src/lib/RailViz.svelte index a85db2ef9..81b279a3b 100644 --- a/ui/src/routes/RailViz.svelte +++ b/ui/src/lib/RailViz.svelte @@ -10,11 +10,12 @@ import { formatTime } from '$lib/toDateTime'; import { lngLatToStr } from '$lib/lngLatToStr'; import maplibregl from 'maplibre-gl'; - import { onDestroy } from 'svelte'; + import { onDestroy, untrack } from 'svelte'; import Control from '$lib/map/Control.svelte'; import { Button } from '$lib/components/ui/button'; import Palette from 'lucide-svelte/icons/palette'; import Rss from 'lucide-svelte/icons/rss'; + import { browser } from '$app/environment'; let { map, @@ -29,6 +30,7 @@ } = $props(); let colorMode = $state<'rt' | 'route'>('route'); + let railvizError = $state(); type RGBA = [number, number, number, number]; @@ -203,7 +205,6 @@ }); }; - let railvizError = $state(); let animation: number | null = null; const updateRailvizLayer = async () => { try { @@ -272,14 +273,16 @@ map.addControl(overlay); console.log('updateRailviz: init'); - updateRailviz(); + untrack(() => updateRailviz()); } }); $effect(() => { if (overlay && bounds && zoom && colorMode) { - console.log(`updateRailviz: effect ${overlay} ${bounds} ${zoom} ${colorMode}`); - updateRailviz(); + untrack(() => { + console.log(`updateRailviz: effect ${overlay} ${bounds} ${zoom} ${colorMode}`); + updateRailviz(); + }); } }); @@ -294,7 +297,7 @@ }); - + -
+
- + - - - - - - +
+ + + + + + +
diff --git a/ui/src/routes/StopTimes.svelte b/ui/src/lib/StopTimes.svelte similarity index 57% rename from ui/src/routes/StopTimes.svelte rename to ui/src/lib/StopTimes.svelte index af60fb831..32770d4c1 100644 --- a/ui/src/routes/StopTimes.svelte +++ b/ui/src/lib/StopTimes.svelte @@ -1,38 +1,58 @@ -
+
@@ -74,28 +92,12 @@ {@const scheduledTimestamp = arriveBy ? t.place.scheduledArrival! : t.place.scheduledDeparture!} - -
diff --git a/ui/src/lib/Time.svelte b/ui/src/lib/Time.svelte index f54954b52..5af2fb81d 100644 --- a/ui/src/lib/Time.svelte +++ b/ui/src/lib/Time.svelte @@ -23,7 +23,7 @@ const lowDelay = $derived(isRealtime && delayMinutes <= 3); -
+
{#if variant == 'schedule'} {formatTime(scheduled)} {:else if variant === 'realtime-show-always' || (variant === 'realtime' && isRealtime)} diff --git a/ui/src/lib/components/ui/card/card.svelte b/ui/src/lib/components/ui/card/card.svelte index c7531d50c..823ff02c7 100644 --- a/ui/src/lib/components/ui/card/card.svelte +++ b/ui/src/lib/components/ui/card/card.svelte @@ -13,7 +13,7 @@
{@render children?.()} diff --git a/ui/src/lib/components/ui/radio-group/radio-group.svelte b/ui/src/lib/components/ui/radio-group/radio-group.svelte index 47172bdbd..3a8b95a76 100644 --- a/ui/src/lib/components/ui/radio-group/radio-group.svelte +++ b/ui/src/lib/components/ui/radio-group/radio-group.svelte @@ -12,4 +12,4 @@ export { className as class }; - + diff --git a/ui/src/lib/components/ui/toggle/toggle.svelte b/ui/src/lib/components/ui/toggle/toggle.svelte index 9c76a8600..9f20638fe 100644 --- a/ui/src/lib/components/ui/toggle/toggle.svelte +++ b/ui/src/lib/components/ui/toggle/toggle.svelte @@ -2,7 +2,7 @@ import { type VariantProps, tv } from "tailwind-variants"; export const toggleVariants = tv({ - base: "hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + base: "hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-blue-600 inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", variants: { variant: { default: "bg-transparent", diff --git a/ui/src/lib/i18n/de.ts b/ui/src/lib/i18n/de.ts index 243917484..12e4f01e5 100644 --- a/ui/src/lib/i18n/de.ts +++ b/ui/src/lib/i18n/de.ts @@ -35,7 +35,8 @@ const translations: Translations = { }, sharingProvider: 'Anbieter', roundtripStationReturnConstraint: - 'Das Fahrzeug muss wieder an der Abfahrtsstation abgestellt werden.' + 'Das Fahrzeug muss wieder an der Abfahrtsstation abgestellt werden.', + noItinerariesFound: 'Keine Verbindungen gefunden.' }; export default translations; diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index 83b78da00..9d50650ea 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -30,11 +30,12 @@ const translations: Translations = { case 1: return '1 intermediate stop'; default: - return `${n} intermediate stops`; + return `${n} intermediate stops`; } }, sharingProvider: 'Provider', - roundtripStationReturnConstraint: 'The vehicle must be returned to the departure station.' + roundtripStationReturnConstraint: 'The vehicle must be returned to the departure station.', + noItinerariesFound: 'No itineraries found.' }; export default translations; diff --git a/ui/src/lib/i18n/fr.ts b/ui/src/lib/i18n/fr.ts index 41cdd6087..27124c09d 100644 --- a/ui/src/lib/i18n/fr.ts +++ b/ui/src/lib/i18n/fr.ts @@ -2,7 +2,7 @@ import type { Translations } from './translation'; const translations: Translations = { journeyDetails: 'Détails du voyage', - walk: 'Marche', + walk: 'à pied', bike: 'Vélo', cargoBike: 'Vélo Cargo', scooterStanding: 'Trottinette', @@ -17,9 +17,9 @@ const translations: Translations = { arrivals: 'Arrivées', later: 'plus tard', earlier: 'plus tôt', - departures: 'Départ', - switchToArrivals: 'Basculer vers les arrivées', - switchToDepartures: 'Basculer vers les départs', + departures: 'Départs', + switchToArrivals: 'Afficher les arrivées', + switchToDepartures: 'Afficher les départs', track: 'Voie', arrivalOnTrack: 'Arrivée sur la voie', tripIntermediateStops: (n: number) => { @@ -33,8 +33,9 @@ const translations: Translations = { } }, sharingProvider: 'Fournisseur', - transfers: 'Transferts', - roundtripStationReturnConstraint: 'Le véhicule doit être retourné à la station de départ.' + transfers: 'correspondances', + roundtripStationReturnConstraint: 'Le véhicule doit être retourné à la station de départ.', + noItinerariesFound: 'Aucun itinéraire trouvé.' }; export default translations; diff --git a/ui/src/lib/i18n/pl.ts b/ui/src/lib/i18n/pl.ts index b40395795..fc52016a8 100644 --- a/ui/src/lib/i18n/pl.ts +++ b/ui/src/lib/i18n/pl.ts @@ -34,7 +34,8 @@ const translations: Translations = { } }, sharingProvider: 'Dostawca', - roundtripStationReturnConstraint: 'Pojazd musi zostać zwrócony do stacji początkowej.' + roundtripStationReturnConstraint: 'Pojazd musi zostać zwrócony do stacji początkowej.', + noItinerariesFound: 'No itineraries found.' }; export default translations; diff --git a/ui/src/lib/i18n/translation.ts b/ui/src/lib/i18n/translation.ts index 6aa91b58e..d2e220249 100644 --- a/ui/src/lib/i18n/translation.ts +++ b/ui/src/lib/i18n/translation.ts @@ -30,6 +30,7 @@ export type Translations = { tripIntermediateStops: (n: number) => string; sharingProvider: string; roundtripStationReturnConstraint: string; + noItinerariesFound: string; }; const translations: Map = new Map( diff --git a/ui/src/lib/map/Control.svelte b/ui/src/lib/map/Control.svelte index 03bbb7a39..8ec08e900 100644 --- a/ui/src/lib/map/Control.svelte +++ b/ui/src/lib/map/Control.svelte @@ -42,7 +42,7 @@
{#if children} diff --git a/ui/src/lib/map/Map.svelte b/ui/src/lib/map/Map.svelte index d783c908f..619ae60a9 100644 --- a/ui/src/lib/map/Map.svelte +++ b/ui/src/lib/map/Map.svelte @@ -64,8 +64,9 @@ }); tmp.on('moveend', async () => { - bounds = tmp.getBounds(); zoom = tmp.getZoom(); + currentZoom = zoom; + bounds = tmp.getBounds(); }); return { diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte index 4186d783f..aa5c8428c 100644 --- a/ui/src/routes/+page.svelte +++ b/ui/src/routes/+page.svelte @@ -3,12 +3,11 @@ import { getStyle } from '$lib/map/style'; import Map from '$lib/map/Map.svelte'; import Control from '$lib/map/Control.svelte'; - import SearchMask from './SearchMask.svelte'; + import SearchMask from '$lib/SearchMask.svelte'; import { posToLocation, type Location } from '$lib/Location'; import { Card } from '$lib/components/ui/card'; import { initial, - type Itinerary, type Match, plan, type PlanResponse, @@ -16,27 +15,30 @@ type Mode, type PlanData } from '$lib/openapi'; - import ItineraryList from './ItineraryList.svelte'; - import ConnectionDetail from './ConnectionDetail.svelte'; + import ItineraryList from '$lib/ItineraryList.svelte'; + import ConnectionDetail from '$lib/ConnectionDetail.svelte'; import { Button } from '$lib/components/ui/button'; - import ItineraryGeoJson from './ItineraryGeoJSON.svelte'; + import ItineraryGeoJson from '$lib/ItineraryGeoJSON.svelte'; import maplibregl from 'maplibre-gl'; import { browser } from '$app/environment'; import { cn } from '$lib/utils'; - import Debug from './Debug.svelte'; + import Debug from '$lib/Debug.svelte'; import Marker from '$lib/map/Marker.svelte'; import Popup from '$lib/map/Popup.svelte'; - import LevelSelect from './LevelSelect.svelte'; + import LevelSelect from '$lib/LevelSelect.svelte'; import { lngLatToStr } from '$lib/lngLatToStr'; import { client } from '$lib/openapi'; - import StopTimes from './StopTimes.svelte'; - import { onMount } from 'svelte'; - import RailViz from './RailViz.svelte'; + import StopTimes from '$lib/StopTimes.svelte'; + import { onMount, tick } from 'svelte'; + import RailViz from '$lib/RailViz.svelte'; import { t } from '$lib/i18n/translation'; + import { pushState, replaceState } from '$app/navigation'; + import { page } from '$app/state'; const urlParams = browser ? new URLSearchParams(window.location.search) : undefined; const hasDebug = urlParams && urlParams.has('debug'); const hasDark = urlParams && urlParams.has('dark'); + const isSmallScreen = browser && window.innerWidth < 600; let theme: 'light' | 'dark' = (hasDark ? 'dark' : undefined) ?? @@ -53,7 +55,7 @@ let bounds = $state(); let map = $state(); - onMount(() => { + onMount(async () => { initial().then((d) => { const r = d.data; if (r) { @@ -61,8 +63,28 @@ zoom = r.zoom; } }); + await tick(); + applyPageStateFromURL(); }); + const applyPageStateFromURL = () => { + if (browser && urlParams) { + if (urlParams.has('tripId')) { + onClickTrip(urlParams.get('tripId')!, true); + } + if (urlParams.has('stopId')) { + const time = urlParams.has('time') ? new Date(urlParams.get('time')!) : new Date(); + onClickStop( + '', + urlParams.get('stopId')!, + time, + urlParams.get('stopArriveBy') == 'true', + true + ); + } + } + }; + let fromParam: Match | undefined = undefined; let toParam: Match | undefined = undefined; if (browser && urlParams && urlParams.has('from') && urlParams.has('to')) { @@ -123,15 +145,18 @@ let searchDebounceTimer: number; let baseResponse = $state>(); let routingResponses = $state>>([]); + let stopNameFromResponse = $state(''); $effect(() => { if (baseQuery) { clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(() => { - const base = plan(baseQuery).then((response) => response.data); + const base = plan(baseQuery).then((response) => { + if (response.error) throw new Error(String(response.error)); + return response.data!; + }); baseResponse = base; routingResponses = [base]; - selectedItinerary = undefined; - selectedStop = undefined; + replaceState('?', {}); }, 400); } }); @@ -149,34 +174,61 @@ }); } - let selectedItinerary = $state(); $effect(() => { - if (selectedItinerary && map) { - const start = maplibregl.LngLat.convert(selectedItinerary.legs[0].from); + if (page.state.selectedItinerary && map) { + const start = maplibregl.LngLat.convert(page.state.selectedItinerary.legs[0].from); const box = new maplibregl.LngLatBounds(start, start); - selectedItinerary.legs.forEach((l) => { + page.state.selectedItinerary.legs.forEach((l) => { box.extend(l.from); box.extend(l.to); l.intermediateStops?.forEach((x) => { box.extend(x); }); }); - const padding = { top: 96, right: 96, bottom: 96, left: 640 }; + const padding = { top: 96, right: 96, bottom: 96, left: isSmallScreen ? 96 : 640 }; map.flyTo({ ...map.cameraForBounds(box), padding }); } }); - let stopArriveBy = $state(); - let selectedStop = $state<{ name: string; stopId: string; time: Date }>(); + const pushStateWithQueryString = ( + // eslint-disable-next-line + queryParams: Record, + // eslint-disable-next-line + newState: App.PageState, + replace: boolean = false + ) => { + const params = new URLSearchParams(queryParams); + const updateState = replace ? replaceState : pushState; + updateState('?' + params.toString(), newState); + }; - const onClickTrip = async (tripId: string) => { + const onClickStop = ( + name: string, + stopId: string, + time: Date, + arriveBy: boolean = false, + replace: boolean = false + ) => { + pushStateWithQueryString( + { stopArriveBy: arriveBy, stopId, time: time.toISOString() }, + { + stopArriveBy: arriveBy, + selectedStop: { name, stopId, time }, + selectedItinerary: page.state.selectedItinerary, + tripId: page.state.tripId + }, + replace + ); + }; + + const onClickTrip = async (tripId: string, replace: boolean = false) => { const { data: itinerary, error } = await trip({ query: { tripId } }); if (error) { - alert(error); + console.log(error); + alert(String((error as Record).error ?? error)); return; } - selectedItinerary = itinerary; - selectedStop = undefined; + pushStateWithQueryString({ tripId }, { selectedItinerary: itinerary, tripId: tripId }, replace); }; type CloseFn = () => void; @@ -218,7 +270,7 @@ } }} {center} - class={cn('h-screen overflow-clip', theme)} + class={cn('h-dvh overflow-clip', theme)} style={getStyle(theme, level)} > {#if hasDebug} @@ -227,80 +279,91 @@ {/if} - - - - - + {#if !isSmallScreen || (!page.state.selectedItinerary && !page.state.selectedStop)} + + + + + + {/if} - {#if !selectedItinerary && routingResponses.length !== 0} - + {#if !page.state.selectedItinerary && routingResponses.length !== 0} + - + pushState('', { selectedItinerary })} + /> {/if} - {#if selectedItinerary && !selectedStop} - - + {#if page.state.selectedItinerary && !page.state.selectedStop} + +

{t.journeyDetails}

-
- { - stopArriveBy = false; - selectedStop = { name, stopId, time }; - }} - {onClickTrip} - /> +
+
- + {/if} - {#if selectedStop} - - + {#if page.state.selectedStop} + +

- {#if stopArriveBy} + {#if page.state.stopArriveBy} {t.arrivals} {:else} {t.departures} {/if} in - {selectedStop.name} + {stopNameFromResponse}

-
+
+ onClickStop( + page.state.selectedStop!.name, + page.state.selectedStop!.stopId, + page.state.selectedStop!.time, + arriveBy + )} {onClickTrip} />