Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
099c937
tag the queries that actually make nw reqs
michaeljwalla Aug 15, 2026
4a193dd
basic state manager and popup
michaeljwalla Aug 15, 2026
163b558
another style (w Empty)
michaeljwalla Aug 16, 2026
c33b8c3
caching mechanism (still need to show)
michaeljwalla Aug 16, 2026
7f34e7a
proxy useRoutes() to attempt to pass cachedRoutes
michaeljwalla Aug 16, 2026
dd75cde
5 hours of coding will save you from 5 minutes of documentation or so…
michaeljwalla Aug 16, 2026
5e302e1
requery loops w backoff and concrete status resolution
michaeljwalla Aug 16, 2026
0bac410
move to ConnectionBar component + relevant changes
michaeljwalla Aug 16, 2026
7ed5c6b
make pill; claude did the anims
michaeljwalla Aug 16, 2026
a3671d4
simpler anim alternative for prior commit
michaeljwalla Aug 16, 2026
75ccc57
dev button for invalidating cache (regen uuids)
michaeljwalla Aug 16, 2026
b774c89
(more) explicit debugging mode for the con man
michaeljwalla Aug 17, 2026
1926396
cache checking funcs
michaeljwalla Aug 17, 2026
b3f3a19
add placeholderData field to both features (for consistency)
michaeljwalla Aug 17, 2026
b89956c
an example proposal basing pages off [id] (which is pretty bad)
michaeljwalla Aug 17, 2026
a842413
initial move to routeCode
michaeljwalla Aug 17, 2026
bdf6fec
some more work to align cached/live state. also surfaced a small bug …
michaeljwalla Aug 17, 2026
eb9b928
connection pill flickering on launch
michaeljwalla Aug 17, 2026
6522989
time-based cache expiry
michaeljwalla Aug 17, 2026
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
2 changes: 2 additions & 0 deletions android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ android {

apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-dialog')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-geolocation')
implementation project(':capacitor-preferences')
Expand Down
6 changes: 6 additions & 0 deletions android/capacitor.settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.0_patch_hash=5f151b861cf2cde88167eece495aecb0862a61bea5115b27870_874123a7c3556940d002a867e09a2b84/node_modules/@capacitor/android/capacitor')

include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.1_@capacitor+core@8.3.0/node_modules/@capacitor/app/android')

include ':capacitor-dialog'
project(':capacitor-dialog').projectDir = new File('../node_modules/.pnpm/@capacitor+dialog@8.0.1_@capacitor+core@8.3.0/node_modules/@capacitor/dialog/android')

include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.0/node_modules/@capacitor/filesystem/android')

Expand Down
4 changes: 4 additions & 0 deletions ios/App/CapApp-SPM/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.3.0"),
.package(name: "CapacitorApp", path: "../../../node_modules/.pnpm/@capacitor+app@8.1.1_@capacitor+core@8.3.0/node_modules/@capacitor/app"),
.package(name: "CapacitorDialog", path: "../../../node_modules/.pnpm/@capacitor+dialog@8.0.1_@capacitor+core@8.3.0/node_modules/@capacitor/dialog"),
.package(name: "CapacitorFilesystem", path: "../../../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.0/node_modules/@capacitor/filesystem"),
.package(name: "CapacitorGeolocation", path: "../../../node_modules/.pnpm/@capacitor+geolocation@8.0.0_@capacitor+core@8.3.0/node_modules/@capacitor/geolocation"),
.package(name: "CapacitorPreferences", path: "../../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.0/node_modules/@capacitor/preferences")
Expand All @@ -22,6 +24,8 @@ let package = Package(
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "CapacitorApp", package: "CapacitorApp"),
.product(name: "CapacitorDialog", package: "CapacitorDialog"),
.product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"),
.product(name: "CapacitorGeolocation", package: "CapacitorGeolocation"),
.product(name: "CapacitorPreferences", package: "CapacitorPreferences")
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
},
"dependencies": {
"@capacitor/android": "8.3.0",
"@capacitor/app": "^8.1.1",
"@capacitor/core": "^8.3.0",
"@capacitor/dialog": "^8.0.1",
"@capacitor/filesystem": "^8.1.2",
"@capacitor/geolocation": "^8.0.0",
"@capacitor/ios": "^8.3.0",
Expand Down
24 changes: 24 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions src/lib/components/ConnectionPill.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import * as Card from './ui/card';
import { onMount } from 'svelte';
import * as Empty from './ui/empty';
import { connectivityManager, ConnectionStatus } from '$lib/managers/connectivity.manager.svelte';
import { CloudOff } from '@lucide/svelte';
import { Spinner } from './ui/spinner';

type Props = {
animateTime?: number;
initialHideTime?: number; // prevent popup from showing on mount (ex, at homepage startup)
offset?: string; // gap between the safe area (notch/camera cutout) and the pill
};

const { animateTime = 500, initialHideTime = 0, offset = '0.75rem' }: Props = $props();

let mayShow = $state(false);
let contentWidth = $state(0);

const status = $derived.by(connectivityManager.getConnectionStatus);
const isBadStatus = $derived(status !== ConnectionStatus.ONLINE);

onMount(() => {
if (initialHideTime > 0) {
mayShow = false;
setTimeout(() => (mayShow = true), initialHideTime);
}
});
</script>

{#if mayShow && isBadStatus}
{@const Icon = status === ConnectionStatus.OFFLINE ? CloudOff : Spinner}
<div
class="pointer-events-none fixed inset-x-0 top-0 z-50 flex justify-center"
style="
padding-top: calc(env(safe-area-inset-top, 0px) + {offset});
padding-left: calc(env(safe-area-inset-left, 0px) + 0.75rem);
padding-right: calc(env(safe-area-inset-right, 0px) + 0.75rem);
"
>
<div
in:fly={{ y: -100, duration: animateTime, opacity: 1 }}
out:fly={{ y: -100, duration: animateTime, opacity: 1 }}
>
<Card.Root
class="flex-row items-center overflow-hidden rounded-full shadow-md transition-[width] duration-300 ease-out motion-reduce:transition-none"
style="width: {contentWidth ? `${contentWidth + 2}px` : 'auto'}"
>
<div
bind:clientWidth={contentWidth}
class="flex w-max shrink-0 flex-row items-center gap-2 py-2 pr-4 pl-2 whitespace-nowrap"
>
<Empty.Media variant="icon" class="mb-0 size-7 rounded-full">
<Icon class="size-4" />
</Empty.Media>
<Empty.Title class="text-sm font-semibold tracking-wide">
{ConnectionStatus.asMessage(status)}
</Empty.Title>
</div>
</Card.Root>
</div>
</div>
{/if}
12 changes: 6 additions & 6 deletions src/lib/components/map/MapElements.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { goto } from '$app/navigation';

const routes = useRoutes();
const drawnRouteIds = $derived(mapManager.drawnRoutes.map((route) => route.id));
const drawnRouteIds = $derived(mapManager.drawnRoutes.map((route) => route.routeCode));
const busLocations = $derived(useVehicles(() => ({ route: mapManager.selectedRoute })));

let userLocation: Location | null = $state(null);
Expand Down Expand Up @@ -58,21 +58,21 @@
flushScheduled = false;
const topmost = requestedRoutes.reduce((a, b) => (b.index > a.index ? b : a)).route;
requestedRoutes.length = 0; //cursed
goto(`/route/${topmost.id}`);
goto(`/route/${topmost.routeCode}`);
}
</script>

{#snippet routeLine(route: Route, index: number)}
{#each route.directions as direction (`${route.id}-${direction.id}`)}
{#each route.directions as direction (`${route.routeCode}-${direction.id}`)}
{@const isSelected =
direction.id === mapManager.selectedDirectionId || mapManager.selectedDirectionId == ''}
{@const isShown = drawnRouteIds.includes(route.id)}
{@const isShown = drawnRouteIds.includes(route.routeCode)}
{@const opacity = isShown ? (isSelected ? 1 : 0.5) : 0}

<MapRoute
coordinates={direction.pathPoints.map((point) => [point.longitude, point.latitude])}
color={getRouteTint(route, themeManager.theme)}
id={`${route.id}-${direction.id}`}
id={`${route.routeCode}-${direction.id}`}
onclick={() => {
if (!isSelected) return; //opacity check instead of isShown for ghost directional routes
requestRoute(route, index);
Expand Down Expand Up @@ -103,7 +103,7 @@
</MapMarker>
{/snippet}

{#each routes?.data as route, i (route.id)}
{#each routes?.data as route, i (route.routeCode)}
{@render routeLine(route, i)}
{/each}

Expand Down
37 changes: 36 additions & 1 deletion src/lib/components/ui/map/MapControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
import { mapManager } from '$lib/managers/map.manager.svelte';
import { cn } from '$lib/utils.js';
import { Geolocation } from '@capacitor/geolocation';
import { Locate, LocateFixed } from '@lucide/svelte';
import { Braces, Locate, LocateFixed } from '@lucide/svelte';
import { getContext } from 'svelte';
import Button from '../button/button.svelte';
import Spinner from '../spinner/spinner.svelte';
import type { MapContext } from './Map.svelte';
import { connectivityManager } from '$lib/managers/connectivity.manager.svelte';
import { Dialog } from '@capacitor/dialog';
import { Capacitor, CapacitorException } from '@capacitor/core';
import { App } from '@capacitor/app';
interface Props {
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
showZoom?: boolean;
Expand Down Expand Up @@ -67,6 +71,25 @@
waitingForLocation = false;
}
}
async function handleCacheScramble() {
const { message, status } = await connectivityManager.scrambleCacheUUIDS();
await Dialog.alert({
title: 'Cache Scrambler',
message: message,
});
if (!status) return;
// true = exit
try {
await App.exitApp();
} catch (error) {
if (!(error instanceof CapacitorException)) return;
await Dialog.alert({
title: 'Cache Scrambler',
message: 'assuming webpage... reloading',
});
window.location.reload();
}
}
</script>

{#if loaded}
Expand All @@ -86,5 +109,17 @@
<Locate class="size-6 stroke-muted-foreground" />
{/if}
</Button>
<!-- for uuid scrambling -->
{#if connectivityManager.isDevMode}
<Button
variant="outline"
size="icon-xl"
onclick={handleCacheScramble}
aria-label="Locate"
class="dark:bg-card"
>
<Braces class="size-6 stroke-muted-foreground"></Braces>
</Button>
{/if}
</div>
{/if}
9 changes: 9 additions & 0 deletions src/lib/data/api/aggie_spirit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const useAuthCodeAPI = () => {
const query = createLoggingQuery<string>(() => ({
label: ASAPIQueryKey.AUTH_CODE,
queryKey: [ASAPIQueryKey.AUTH_CODE],
meta: { network: true, auth: true },
queryFn: async () => {
const authCodeB64 = await (await fetch('https://auth.maroonrides.app')).text();
return atob(authCodeB64);
Expand All @@ -60,6 +61,7 @@ export const useAuthTokenAPI = () => {

const query = createDependencyQuery<Headers>(() => ({
queryKey: [ASAPIQueryKey.AUTH_TOKEN],
meta: { network: true },
queryFn: async () => {
var res = await fetch('https://aggiespirit.ts.tamu.edu/', { credentials: 'omit' });

Expand All @@ -84,6 +86,7 @@ export const useRoutePlanAuthTokenAPI = (params: () => { queryString: string })
const { queryString } = params();
return {
queryKey: [ASAPIQueryKey.ROUTE_PLAN_AUTH_TOKEN],
meta: { network: true },
queryFn: async () => {
var res = await fetch(`https://aggiespirit.ts.tamu.edu/TripPlanner/${queryString}`, {
credentials: 'omit',
Expand Down Expand Up @@ -111,6 +114,7 @@ export const useBaseDataAPI = () => {

const query = createDependencyQuery<IGetBaseDataResponse>(() => ({
queryKey: [ASAPIQueryKey.BASE_DATA],
meta: { network: true },
queryFn: async () => {
const baseData = await getBaseData(authTokenQuery.data!);
GetBaseDataResponseSchema.parse(baseData);
Expand All @@ -130,6 +134,7 @@ export const usePatternPathsAPI = () => {

const query = createDependencyQuery<IGetPatternPathsResponse>(() => ({
queryKey: [ASAPIQueryKey.PATTERN_PATHS],
meta: { network: true },
queryFn: async () => {
const baseData = baseDataQuery.data as IGetBaseDataResponse;

Expand Down Expand Up @@ -174,6 +179,7 @@ export const useStopEstimateAPI = (
const { routeKey, directionKey, stopCode } = params();
return {
queryKey: [ASAPIQueryKey.STOP_ESTIMATE, routeKey, directionKey, stopCode],
meta: { network: true },
queryFn: async () => {
const response = await getNextDepartureTimes(
routeKey,
Expand All @@ -200,6 +206,7 @@ export const useTimetableEstimateAPI = (params: () => { stopCode: string; date:
const { stopCode, date } = params();
return {
queryKey: [ASAPIQueryKey.TIMETABLE_ESTIMATE, stopCode, moment(date).format('YYYY-MM-DD')],
meta: { network: true },
queryFn: async () => {
const response = await getStopEstimates(stopCode, date, authTokenQuery.data!);
GetStopEstimatesResponseSchema.parse(response);
Expand All @@ -221,6 +228,7 @@ export const useStopScheduleAPI = (params: () => { stopCode: string; date: Date
const { stopCode, date } = params();
return {
queryKey: [ASAPIQueryKey.STOP_SCHEDULE, stopCode, moment(date).format('YYYY-MM-DD')],
meta: { network: true },
queryFn: async () => {
const response = await getStopSchedules(stopCode, date, authTokenQuery.data!);
GetStopSchedulesResponseSchema.parse(response);
Expand All @@ -242,6 +250,7 @@ export const useVehiclesAPI = (params: () => { routeKey: string }) => {
const { routeKey } = params();
return {
queryKey: [ASAPIQueryKey.VEHICLES, routeKey],
meta: { network: true },
queryFn: async () => {
let busesResponse = (await getVehicles(
[routeKey],
Expand Down
2 changes: 2 additions & 0 deletions src/lib/data/api/route_planning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const useSearchSuggestionAPI = (params: () => { query: string }) => {
const { query } = params();
return {
queryKey: [ASRoutePlanQueryKey.SEARCH_SUGGESTION, query],
meta: { network: true },
queryFn: async () => {
// we need data from pattern paths to get the stop GPS locations
// This is limitation of the API where we can't get the GPS location of a stop directly
Expand Down Expand Up @@ -93,6 +94,7 @@ export const useTripPlanAPI = (
const { origin, destination, date, deadline } = params();
return {
queryKey: [ASRoutePlanQueryKey.TRIP_PLAN, origin, destination, date, deadline],
meta: { network: true },
queryFn: async () => {
let response = await getTripPlan(
routePlanAuthToken.data!,
Expand Down
4 changes: 4 additions & 0 deletions src/lib/data/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
type StopSchedule,
type TimeEstimate,
} from '$lib/data/types';
import { connectivityManager } from '$lib/managers/connectivity.manager.svelte';
import { queryLogger } from '$lib/utils/logger';
import type { CreateQueryResult } from '@tanstack/svelte-query';
import moment from 'moment';
import { createDependencyQuery, createSelectableQuery } from '../utils/queries';
import {
Expand Down Expand Up @@ -38,6 +40,8 @@ export const useRoutes = () => {

const query = createDependencyQuery<Route[]>(() => ({
queryKey: [QueryKey.ROUTE_LIST],
meta: { isRoutes: true },
placeholderData: connectivityManager.cachedRoutes,
queryFn: async () => {
queryLogger.i(`Loaded ${asRouteList.data?.length} routes from Aggie Spirit`);
return asRouteList.data!;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/data/structure/aggie_spirit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const useASStopEstimate = (
queryFn: async () => {
const stopEstimates = apiStopEstimateQuery.data!;

return stopEstimates.routeDirectionTimes[0].nextDeparts.map((e) => {
return (stopEstimates.routeDirectionTimes[0]?.nextDeparts ?? []).map((e) => {
return {
dataSource: DataSource.AGGIE_SPIRIT,
estimatedTime: e.estimatedDepartTimeUtc
Expand Down
Loading
Loading