Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,38 @@
"@hey-api/openapi-ts": "0.95.0",
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.56.1",
"@sveltejs/kit": "^2.57.1",
"@sveltejs/vite-plugin-svelte": "7.0.0",
"@tailwindcss/forms": "^0.5.11",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.8",
"@types/d3-time": "^3.0.4",
"@types/eslint": "^9.6.1",
"@types/node": "^25.5.2",
"@vitest/browser-playwright": "^4.1.0",
"@types/node": "^25.6.0",
"@vitest/browser-playwright": "^4.1.4",
"eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.17.0",
"globals": "^17.4.0",
"maplibre-gl": "^5.22.0",
"playwright": "^1.59.1",
"prettier": "^3.8.1",
"prettier": "^3.8.2",
"prettier-plugin-svelte": "^3.5.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "5.55.1",
"svelte": "5.55.3",
"svelte-check": "^4.4.5",
"svelte-maplibre-gl": "^1.0.3",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.0",
"typescript-eslint": "^8.57.1",
"vite": "^8.0.1",
"vitest": "^4.1.0",
"typescript-eslint": "^8.58.1",
"vite": "^8.0.8",
"vitest": "^4.1.4",
"vitest-browser-svelte": "^2.1.0"
},
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@lucide/svelte": "^1.0.0",
"@lucide/svelte": "^1.8.0",
"@tailwindcss/vite": "^4.2.2",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
Expand Down
682 changes: 343 additions & 339 deletions frontend/pnpm-lock.yaml

Large diffs are not rendered by default.

21 changes: 1 addition & 20 deletions frontend/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
import type { ApiAlert, Route, Source, Stop, StopTime, Trip } from '$lib/client';
import type { AlertResource } from '$lib/resources/alerts.svelte';
import type { PositionResource } from '$lib/resources/positions.svelte';
import type { StopTimesResource } from '$lib/resources/stop_times.svelte';
import type { TripResource } from '$lib/resources/trips.svelte';

interface RealtimeInitialValue<T> {
source: Source;
data: T;
}
import type { Route, Source, Stop, Trip } from '$lib/client';

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// maybe this should be maps
interface PageData {
selected_sources: Source[];
stops: Partial<Record<Source, Stop[]>>;
routes: Partial<Record<Source, Route[]>>;
stops_by_id: Partial<Record<Source, Record<string, Stop>>>;
routes_by_id: Partial<Record<Source, Record<string, Route>>>;
// Initial realtime values as SourceMaps
initial_trips: RealtimeInitialValue<TripResource>[];
initial_stop_times: RealtimeInitialValue<StopTimesResource>[];
initial_positions: RealtimeInitialValue<PositionResource>[];
initial_alerts: RealtimeInitialValue<AlertResource>[];
// Current time param for RT fetches
at?: string;
// used to keep track of the current monitored
// current_monitored_routes: string[];
// initial_promise: Promise<[void, void]>;
}
// <T extends string | number>
interface PageState {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/Icon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
const show_alert_icon = $derived.by(() => {
if (!show_alerts || !alerts) return false;
// TODO: maybe differentiate between planned alerts, station notices, etc
return alerts.value?.alerts_by_route.has(route.id) ?? false;
return alerts.current?.alerts_by_route.has(route.id) ?? false;
});

// TODO: apply this bus prefix thing to map
Expand Down
14 changes: 8 additions & 6 deletions frontend/src/lib/Route/Button.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import Icon from '$lib/Icon.svelte';
import type { Route, Source } from '$lib/client';
import Skeleton from '$lib/Skeleton.svelte';
import type { Route } from '$lib/client';
import { alert_context } from '$lib/resources/alerts.svelte';

interface Props {
Expand All @@ -12,21 +13,23 @@
const alerts = $derived(alert_context.getSource(data.data.source));

const route_alerts = $derived(
alerts?.value?.alerts_by_route
alerts?.current?.alerts_by_route
.get(data.id)
?.sort(
(a, b) =>
b.entities.find((e) => e.route_id === data.id)!.sort_order -
a.entities.find((e) => e.route_id === data.id)!.sort_order
) ?? []
);
// const alerts: any[] = [];

const alerts_loading = $derived(alerts?.status !== 'ready' && !route_alerts.length);
</script>

<!-- <Button state={{ modal: 'route', data }} {pin_rune}> -->
<section class="flex items-center gap-1">
<Icon height={36} width={36} link={true} route={data} />
{#if route_alerts.length}
{#if alerts_loading}
<Skeleton lines={1} class="w-24" />
{:else if route_alerts.length}
<div class="font-semibold">
{#if 'alert_type' in route_alerts[0].data}
{route_alerts[0].data.alert_type}
Expand All @@ -43,4 +46,3 @@
No Alerts
{/if}
</section>
<!-- </Button> -->
2 changes: 1 addition & 1 deletion frontend/src/lib/Route/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
const alerts = $derived(alert_context.getSource(route.data.source));

const route_alerts = $derived(
alerts?.value?.alerts_by_route
alerts?.current?.alerts_by_route
.get(route.id)
?.sort(
(a, b) =>
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/lib/Skeleton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
interface Props {
lines?: number;
class?: string;
}

let { lines = 3, class: className = '' }: Props = $props();
</script>

<div class="flex flex-col gap-2 {className}">
{#each { length: lines } as _, i}
<div
class="h-4 animate-pulse rounded bg-neutral-800"
style:width="{75 + ((i * 17) % 25)}%"
></div>
{/each}
</div>
89 changes: 31 additions & 58 deletions frontend/src/lib/Stop/Button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,26 @@
const trips = $derived(trip_context.getSource(stop.data.source));
const stop_times = $derived(stop_time_context.getSource(stop.data.source));

// TODO: improve this (and handle removing route when unmounted)
$effect(() => {
if (source_info[stop.data.source]?.monitor_routes) {
// TODO: might need to add active_routes that aren't included in the stop.routes array
if (stop_times && source_info[stop.data.source]?.monitor_routes) {
for (const r of stop.routes) {
stop_times?.add_route(r.route_id);
stop_times.add_route(r.route_id);
}
}
});
// stop_times.add_route()

const current_stop_times = $derived(stop_times?.current.by_stop_id.get(stop.id) ?? []);

const is_loading = $derived(
!stop_times || (stop_times.status !== 'ready' && current_stop_times.length === 0)
);

const main_rs = $derived(main_route_stops(stop.routes));

const stop_times_by_direction = $derived.by(() => {
const stop_times_by_direction = new Map<number, StopTimesByRoute>();

// Pre-populate both directions with all routes from stop data
if (stop.data.source === 'mta_subway') {
// TODO: find some way to not hardcode the directions (maybe have this info in the source data or something)
for (const direction of [1, 3]) {
const route_map: StopTimesByRoute = new Map();
for (const route of main_rs) {
Expand All @@ -52,11 +53,9 @@
}
}

const current_stop_times = stop_times?.value?.by_stop_id.get(stop.id) ?? [];
// TODO: maybe end loop early if we get 2 trips for each route or something like that
for (const st of current_stop_times) {
if (st.arrival.getTime() < current_time.ms) continue;
const trip = trips?.value?.get(st.trip_id);
const trip = trips?.current?.get(st.trip_id);
if (!trip) continue;

const route_id = trip.route_id;
Expand All @@ -78,14 +77,11 @@
return stop_times_by_direction;
});

// $inspect(stop_times_by_direction);

const default_stop_routes = $derived(
main_rs.map((r) => routes[r.route_id]).filter((r) => r !== undefined)
);
</script>

<!-- eta used by bus and train -->
{#snippet eta(n: number)}
{@const eta = parseInt(n.toFixed(0))}
{#key eta}
Expand All @@ -94,7 +90,26 @@
</span>
{/key}
{/snippet}
<!-- TODO: show different message depending on if loading or theres actually no trips -->

{#snippet eta_or_loading(route_stop_times: StopTimeWithETA[])}
{#if is_loading}
<span
class="inline-block w-8 animate-pulse rounded-sm bg-neutral-800 px-1.5 py-0.5 text-sm leading-5"
>&nbsp;</span
>
<span
class="inline-block w-10 animate-pulse rounded-sm bg-neutral-800 px-1.5 py-0.5 text-sm leading-5"
>&nbsp;</span
>
{:else if route_stop_times.length}
{#each route_stop_times.slice(0, 2) as stop_time (stop_time.trip_id)}
{@render eta(stop_time.eta)}
{/each}
{:else}
<div class="text-neutral-400">No trips</div>
{/if}
{/snippet}

{#if stop.data.source === 'mta_subway'}
<div class="grid w-full grid-cols-1 gap-1">
<div class="flex items-center gap-1">
Expand All @@ -116,54 +131,19 @@
<div class="flex flex-col gap-1">
{#each stop_times_by_route as [route_id, route_stop_times] (route_id)}
{@const route = routes[route_id]}
<!-- {@const route_stop_times = stop_times.filter((st) => st.route_id === route.id)} -->
<div class="flex items-center gap-1">
<Icon height={20} width={20} link={false} {route} />
<div class="flex items-center gap-1">
{#if route_stop_times.length}
{#each route_stop_times.slice(0, 2) as stop_time (stop_time.trip_id)}
{@render eta(stop_time.eta)}
{/each}
{:else}
<div class="text-neutral-400">No trips</div>
{/if}
{@render eta_or_loading(route_stop_times)}
</div>
</div>
{/each}
</div>
</div>
{/each}
<!-- {@render arrivals(stop.data.north_headsign, all_stop_routes, nb_st_by_route)}
{@render arrivals(stop.data.south_headsign, all_stop_routes, sb_st_by_route)} -->
</div>
</div>

<!-- {#snippet arrivals(headsign: string, routes: Route[], stop_times: StopTimeByRoute)}
<div class="mt-auto flex flex-col">
<div class="table-cell max-w-[85%] text-left font-semibold">
{headsign}
</div>
<div class="flex flex-col gap-1">
{#each routes as route (route.id)}
{@const route_stop_times = stop_times.get(route.id) ?? []}
<div class="flex items-center gap-1">
<Icon height={20} width={20} link={false} {route} />
<div class="flex items-center gap-1">
{#if route_stop_times.length}
{#each route_stop_times.slice(0, 2) as stop_time (stop_time.trip_id)}
{@render eta(stop_time.eta)}
{/each}
{:else}
<div class="text-neutral-400">No trips</div>
{/if}
</div>
</div>
{/each}
</div>
</div>
{/snippet} -->
{:else if ['mta_bus', 'njt_bus'].includes(stop.data.source)}
<!-- TODO: make spacing consistent (use grid maybe idk) -->
<div class="flex flex-col text-white">
<div class="flex items-center">
{#if stop.data.source === 'mta_bus'}
Expand All @@ -189,19 +169,12 @@
<Icon {route} link={false} />
<div class="flex flex-col">
<div>
<!-- TODO: handle other sources -->
{#if 'headsign' in route_stop.data}
{route_stop.data.headsign}
{/if}
</div>
<div class="flex gap-2 pr-1">
{#if route_stop_times.length}
{#each route_stop_times.slice(0, 2) as stop_time (stop_time.trip_id)}
{@render eta(stop_time.eta)}
{/each}
{:else}
<div class="text-neutral-400">No trips</div>
{/if}
{@render eta_or_loading(route_stop_times)}
</div>
</div>
</div>
Expand Down
Loading