From 66779eec17e1a7901bb4e553a8a3ebc9c0ea5f19 Mon Sep 17 00:00:00 2001 From: pir8bay Date: Sat, 22 Jun 2024 01:17:30 +0300 Subject: [PATCH 01/17] init commit --- src/components/RouteCard.tsx | 159 +++++++++++++++--- src/components/RouteStaticMap.tsx | 8 +- src/components/RouteStatistics.tsx | 2 +- src/components/Timeline.tsx | 16 +- src/index.css | 9 + src/map/index.ts | 14 ++ .../dashboard/activities/DeviceActivity.tsx | 2 +- src/types.d.ts | 32 ++-- 8 files changed, 195 insertions(+), 47 deletions(-) diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 91a3a594..362e958a 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -1,20 +1,23 @@ -import { Suspense, type VoidComponent } from 'solid-js' +import { createSignal, createEffect, Suspense, type VoidComponent } from 'solid-js' import dayjs from 'dayjs' import Avatar from '~/components/material/Avatar' -import Card, { CardContent, CardHeader } from '~/components/material/Card' +import { CardContent, CardHeader } from '~/components/material/Card' import Icon from '~/components/material/Icon' import RouteStaticMap from '~/components/RouteStaticMap' import RouteStatistics from '~/components/RouteStatistics' +import Timeline from './Timeline' -import type { RouteSegments } from '~/types' +import type { Route, RouteSegments } from '~/types' -const RouteHeader = (props: { route: RouteSegments }) => { - const startTime = () => dayjs(props.route.segment_start_times[0]) - const endTime = () => dayjs(props.route.segment_end_times.at(-1)) +import { reverseGeocode } from '~/map' - const headline = () => startTime().format('ddd, MMM D, YYYY') - const subhead = () => `${startTime().format('h:mm A')} to ${endTime().format('h:mm A')}` +const RouteHeader = (props: { route?: RouteSegments }) => { + const startTime = () => props?.route?.segment_start_times ? dayjs(props.route.segment_start_times[0]) : null + const endTime = () => props?.route?.segment_end_times ? dayjs(props.route.segment_end_times.at(-1)) : null + + const headline = () => startTime()?.format('ddd, MMM D, YYYY') + const subhead = () => `${startTime()?.format('h:mm A')} to ${endTime()?.format('h:mm A')}` return ( { ) } -interface RouteCardProps { - route: RouteSegments +interface GeoResult { + features?: Array<{ + properties?: { + context?: { + neighborhood?: string | null, + region?: string | null, + place?: string | null + } + } + }> } -const RouteCard: VoidComponent = (props) => { +interface LocationContext { + neighborhood?: { + name: string | null, + }, + region?: { + region_code: string | null, + }, + place?: { + name: string | null, + } +} + +const RouteRevGeo = (props: { route?: Route }) => { + const [startLocation, setStartLocation] = createSignal<{ + neighborhood?: string | null, + region?: string | null + }>({ neighborhood: null, region: null }) + + const [endLocation, setEndLocation] = createSignal<{ + neighborhood?: string | null, + region?: string | null + }>({ neighborhood: null, region: null }) + + const [error, setError] = createSignal(null) + + createEffect(() => { + if (!props.route) return + + const { start_lng, start_lat, end_lng, end_lat } = props.route + + if (!start_lng || !start_lat || !end_lng || !end_lat) return + + const fetchGeoData = async () => { + try { + const start_revGeoResult = await reverseGeocode(start_lng, start_lat) as GeoResult + const end_revGeoResult = await reverseGeocode(end_lng, end_lat) as GeoResult + + if (start_revGeoResult instanceof Error) { + setError(start_revGeoResult as Error) + console.error(start_revGeoResult) + return + } + + if (end_revGeoResult instanceof Error) { + setError(end_revGeoResult as Error) + console.error(end_revGeoResult) + return + } + + const { neighborhood: startNeighborhood, region: startRegion, place: startPlace } = + (start_revGeoResult?.features?.[0]?.properties?.context || {}) as LocationContext + + const { neighborhood: endNeighborhood, region: endRegion, place: endPlace } = + (end_revGeoResult?.features?.[0]?.properties?.context || {}) as LocationContext + + setStartLocation({ + neighborhood: startNeighborhood?.name || startPlace?.name, + region: startRegion?.region_code, + }) + setEndLocation({ + neighborhood: endNeighborhood?.name || endPlace?.name, + region: endRegion?.region_code, + }) + } catch (error) { + setError(error as Error) + console.error(error) + } + } + + fetchGeoData().catch((error) => { + console.error('An error occurred while fetching geolocation data:', error) + }) + }) + return ( - - - -
- } - > - - +
+ {error() &&
Error: {error()?.message}
} +
+ {startLocation() &&
{startLocation()?.neighborhood}, {startLocation()?.region}
} + + arrow_right_alt + + {endLocation() &&
{endLocation()?.neighborhood}, {endLocation()?.region}
}
+
+ ) +} + +type RouteCardProps = { + route?: Route; +} + +const RouteCard: VoidComponent = (props) => { + const route = () => props.route - - - - + return ( + +
+
+ } + > + + +
+ +
+ + + + + + + +
+
+
) } diff --git a/src/components/RouteStaticMap.tsx b/src/components/RouteStaticMap.tsx index d8e83d5d..1a010bea 100644 --- a/src/components/RouteStaticMap.tsx +++ b/src/components/RouteStaticMap.tsx @@ -25,11 +25,13 @@ const getStaticMapUrl = (gpsPoints: GPSPathPoint[]): string | undefined => { if (gpsPoints.length === 0) { return undefined } + const path: Coords = [] gpsPoints.forEach(({ lng, lat }) => { path.push([lng, lat]) }) const themeId = getThemeId() + return getPathStaticMapUrl(themeId, path, 380, 192, true) } @@ -41,7 +43,7 @@ const State = (props: { return (
@@ -64,7 +66,7 @@ const RouteStaticMap: VoidComponent = (props) => { return (
@@ -81,7 +83,7 @@ const RouteStaticMap: VoidComponent = (props) => { diff --git a/src/components/RouteStatistics.tsx b/src/components/RouteStatistics.tsx index 5e0ca456..437efa59 100644 --- a/src/components/RouteStatistics.tsx +++ b/src/components/RouteStatistics.tsx @@ -25,7 +25,7 @@ const RouteStatistics: VoidComponent = (props) => { const [timeline] = createResource(() => props.route, getTimelineStatistics) return ( -
+
Distance {formatRouteDistance(props.route)} diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index 70f1ce3b..84950d34 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -1,9 +1,8 @@ import { For, createResource, Show, Suspense } from 'solid-js' -import type { VoidComponent } from 'solid-js' +import type { Component } from 'solid-js' import clsx from 'clsx' import { TimelineEvent, getTimelineEvents } from '~/api/derived' -import { getRoute } from '~/api/route' import type { Route } from '~/types' import { getRouteDuration } from '~/utils/date' @@ -112,19 +111,20 @@ function renderMarker(route: Route | undefined, seekTime: number | undefined) { } interface TimelineProps { + route: Route | undefined class?: string - routeName: string seekTime?: number + rounded?: string } -const Timeline: VoidComponent = (props) => { - const [route] = createResource(() => props.routeName, getRoute) +const Timeline: Component = (props) => { + const route = () => props.route const [events] = createResource(route, getTimelineEvents) return (
= (props) => { > }> - {(route) => ( + {(route: Route | undefined) => ( <> - {(events) => renderTimelineEvents(route, events)} + {(events: TimelineEvent[]) => renderTimelineEvents(route, events)} {renderMarker(route, props.seekTime)} diff --git a/src/index.css b/src/index.css index e28fa8a6..c9915dc5 100644 --- a/src/index.css +++ b/src/index.css @@ -213,4 +213,13 @@ display: none; /* Safari and Chrome */ } } + + .custom-card { + cursor: pointer; + background-color: var(--color-surface-container-low); + } + + .custom-card:hover { + background-color: var(--color-surface-container); + } } diff --git a/src/map/index.ts b/src/map/index.ts index cd52e234..eb407aba 100644 --- a/src/map/index.ts +++ b/src/map/index.ts @@ -7,6 +7,8 @@ import { MAPBOX_TOKEN, } from './config' +import type { GeocodeResult } from '~/types' + export type Coords = [number, number][] const POLYLINE_SAMPLE_SIZE = 50 @@ -50,3 +52,15 @@ export function getPathStaticMapUrl( )})` return `https://api.mapbox.com/styles/v1/${MAPBOX_USERNAME}/${styleId}/static/${path}/auto/${width}x${height}${hidpiStr}?logo=false&attribution=false&padding=30,30,30,30&access_token=${MAPBOX_TOKEN}` } + +export async function reverseGeocode(lng: number, lat: number): Promise { + const url = `https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}.733&types=address&worldview=us&access_token=${MAPBOX_TOKEN}` + try { + const response = await fetch(url) + const data = await (response.json() as Promise) + return data + } catch (error) { + console.error(error) + throw error + } +} diff --git a/src/pages/dashboard/activities/DeviceActivity.tsx b/src/pages/dashboard/activities/DeviceActivity.tsx index f47638f0..09ee7307 100644 --- a/src/pages/dashboard/activities/DeviceActivity.tsx +++ b/src/pages/dashboard/activities/DeviceActivity.tsx @@ -33,7 +33,7 @@ const DeviceActivity: VoidComponent = (props) => {
-
+
Routes
diff --git a/src/types.d.ts b/src/types.d.ts index cc9d76e4..6dd235d0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -50,10 +50,12 @@ export interface Route { create_time: number devicetype: number dongle_id: string + start_lng?: number + start_lat?: number end_lat?: number end_lng?: number - end_time?: string - fullname: string + end_time: string + fullname?: string git_branch?: string git_commit?: string git_dirty?: boolean @@ -88,14 +90,24 @@ export interface RouteShareSignature extends Record { } export interface RouteSegments extends Route { - end_time_utc_millis: number - is_preserved: boolean - segment_end_times: number[] - segment_numbers: number[] - segment_start_times: number[] - share_exp: RouteShareSignature['exp'] - share_sig: RouteShareSignature['sig'] - start_time_utc_millis: number + end_time_utc_millis?: number + is_preserved?: boolean + segment_end_times?: number[] + segment_numbers?: number[] + segment_start_times?: number[] + share_exp?: RouteShareSignature['exp'] + share_sig?: RouteShareSignature['sig'] + start_time_utc_millis?: number +} + +export interface GeocodeResult { + formatted_address: string; + geometry: { + location: { + lat: number; + lng: number; + }; + }; } export interface Clip { From e83adbf7366208c76546ab08f692c9f5e487b93a Mon Sep 17 00:00:00 2001 From: Sarem Hailemeskel <166467953+sarem-h@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:27:16 +0000 Subject: [PATCH 02/17] refactor --- src/components/RouteCard.tsx | 96 +++++++++++++++++------------------- 1 file changed, 45 insertions(+), 51 deletions(-) diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 362e958a..90c0ea85 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -56,64 +56,58 @@ interface LocationContext { } } -const RouteRevGeo = (props: { route?: Route }) => { - const [startLocation, setStartLocation] = createSignal<{ - neighborhood?: string | null, - region?: string | null - }>({ neighborhood: null, region: null }) +async function fetchGeoData(lng: number, lat: number): Promise { + try { + const revGeoResult = await reverseGeocode(lng, lat) as GeoResult + if (revGeoResult instanceof Error) throw revGeoResult + return revGeoResult + } catch (error) { + console.error(error) + // To allow execution to continue for the next location. + return null + } +} + +function processGeoResult( + result: GeoResult | null, + setLocation: (location: { neighborhood?: string | null, region?: string | null }) => void, +) { + if (result) { + const { neighborhood, region, place } = + (result?.features?.[0]?.properties?.context || {}) as LocationContext + setLocation({ + neighborhood: neighborhood?.name || place?.name, + region: region?.region_code, + }) + } +} - const [endLocation, setEndLocation] = createSignal<{ - neighborhood?: string | null, - region?: string | null - }>({ neighborhood: null, region: null }) +type LocationState = { neighborhood?: string | null, region?: string | null } +const RouteRevGeo = (props: { route?: Route }) => { + const [startLocation, setStartLocation] = createSignal({ + neighborhood: null, + region: null, + }) + const [endLocation, setEndLocation] = createSignal({ + neighborhood: null, + region: null, + }) const [error, setError] = createSignal(null) createEffect(() => { if (!props.route) return - const { start_lng, start_lat, end_lng, end_lat } = props.route - if (!start_lng || !start_lat || !end_lng || !end_lat) return - const fetchGeoData = async () => { - try { - const start_revGeoResult = await reverseGeocode(start_lng, start_lat) as GeoResult - const end_revGeoResult = await reverseGeocode(end_lng, end_lat) as GeoResult - - if (start_revGeoResult instanceof Error) { - setError(start_revGeoResult as Error) - console.error(start_revGeoResult) - return - } - - if (end_revGeoResult instanceof Error) { - setError(end_revGeoResult as Error) - console.error(end_revGeoResult) - return - } - - const { neighborhood: startNeighborhood, region: startRegion, place: startPlace } = - (start_revGeoResult?.features?.[0]?.properties?.context || {}) as LocationContext - - const { neighborhood: endNeighborhood, region: endRegion, place: endPlace } = - (end_revGeoResult?.features?.[0]?.properties?.context || {}) as LocationContext - - setStartLocation({ - neighborhood: startNeighborhood?.name || startPlace?.name, - region: startRegion?.region_code, - }) - setEndLocation({ - neighborhood: endNeighborhood?.name || endPlace?.name, - region: endRegion?.region_code, - }) - } catch (error) { - setError(error as Error) - console.error(error) - } - } - - fetchGeoData().catch((error) => { + Promise.all([ + fetchGeoData(start_lng, start_lat), + fetchGeoData(end_lng, end_lat), + ]).then(([startResult, endResult]) => { + processGeoResult(startResult, setStartLocation) + processGeoResult(endResult, setEndLocation) + }).catch((error) => { + setError(error as Error) console.error('An error occurred while fetching geolocation data:', error) }) }) @@ -122,11 +116,11 @@ const RouteRevGeo = (props: { route?: Route }) => {
{error() &&
Error: {error()?.message}
}
- {startLocation() &&
{startLocation()?.neighborhood}, {startLocation()?.region}
} + {startLocation().neighborhood &&
{startLocation().neighborhood}, {startLocation().region}
} arrow_right_alt - {endLocation() &&
{endLocation()?.neighborhood}, {endLocation()?.region}
} + {endLocation().neighborhood &&
{endLocation().neighborhood}, {endLocation().region}
}
) From b99492d294d5f59fe00070274acc7a2d0cf0b175 Mon Sep 17 00:00:00 2001 From: Sarem Hailemeskel <166467953+sarem-h@users.noreply.github.com> Date: Sat, 22 Jun 2024 11:02:33 +0000 Subject: [PATCH 03/17] fixed reverseGeocode url type --- src/map/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/map/index.ts b/src/map/index.ts index eb407aba..cada0f11 100644 --- a/src/map/index.ts +++ b/src/map/index.ts @@ -54,7 +54,7 @@ export function getPathStaticMapUrl( } export async function reverseGeocode(lng: number, lat: number): Promise { - const url = `https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}.733&types=address&worldview=us&access_token=${MAPBOX_TOKEN}` + const url = `https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}&types=address&worldview=us&access_token=${MAPBOX_TOKEN}` try { const response = await fetch(url) const data = await (response.json() as Promise) From 712d714cb1ce6362d3ecd4394ab8242983c19e76 Mon Sep 17 00:00:00 2001 From: pir8bay Date: Sat, 6 Jul 2024 13:06:10 +0300 Subject: [PATCH 04/17] added route like and options UI --- src/components/RouteCard.tsx | 125 +++++++++++++++++++++++++----- src/components/RouteOptions.tsx | 68 ++++++++++++++++ src/components/RouteStaticMap.tsx | 2 +- src/index.css | 53 +++++++++++++ 4 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 src/components/RouteOptions.tsx diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 90c0ea85..2ac9b951 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -1,9 +1,10 @@ -import { createSignal, createEffect, Suspense, type VoidComponent } from 'solid-js' +import { createSignal, createEffect, Suspense, Show, type Component } from 'solid-js' import dayjs from 'dayjs' import Avatar from '~/components/material/Avatar' import { CardContent, CardHeader } from '~/components/material/Card' import Icon from '~/components/material/Icon' +import RouteOptions from '~/components/RouteOptions' import RouteStaticMap from '~/components/RouteStaticMap' import RouteStatistics from '~/components/RouteStatistics' import Timeline from './Timeline' @@ -12,13 +13,84 @@ import type { Route, RouteSegments } from '~/types' import { reverseGeocode } from '~/map' +type RouteOptionsProps = { + route?: Route; +} + +const [showRouteOptionsCard, setShowRouteOptionsCard] = createSignal(false) + +const RouteOptionsCard: Component = (props) => { + const [isMdOrLarger, setIsMdOrLarger] = createSignal(false) + + // listen isMdOrLarger + createEffect(() => { + const updateSize = () => { + setIsMdOrLarger(window.innerWidth >= 768) + } + window.addEventListener('resize', updateSize) + // Initial check + updateSize() + + return () => window.removeEventListener('resize', updateSize) + }) + + const stopPropagation = (event: MouseEvent) => { + event.stopPropagation() + + setShowRouteOptionsCard(false) + } + + return ( + +
+
event.stopPropagation()}> + +
+
+
+ ) +} + +type FavoriteRoutes = string[] + const RouteHeader = (props: { route?: RouteSegments }) => { + const startTime = () => props?.route?.segment_start_times ? dayjs(props.route.segment_start_times[0]) : null const endTime = () => props?.route?.segment_end_times ? dayjs(props.route.segment_end_times.at(-1)) : null const headline = () => startTime()?.format('ddd, MMM D, YYYY') const subhead = () => `${startTime()?.format('h:mm A')} to ${endTime()?.format('h:mm A')}` + const [isFavorite, setIsFavorite] = createSignal(false) + + const toggleFavorite = (routeName: string) => { + let favorites: FavoriteRoutes = JSON.parse(localStorage.getItem('favoriteRoutes') || '[]') as FavoriteRoutes + const isFavorite = favorites.includes(routeName) + favorites = isFavorite ? favorites.filter(name => name !== routeName) : [...favorites, routeName] + localStorage.setItem('favoriteRoutes', JSON.stringify(favorites)) + setIsFavorite(!isFavorite) + } + + createEffect(() => { + const favorites: FavoriteRoutes = JSON.parse(localStorage.getItem('favoriteRoutes') || '[]') as FavoriteRoutes + setIsFavorite(favorites.includes(props.route?.fullname ?? '')) + }) + + const handleLikeClick = (event: MouseEvent) => { + event.stopPropagation() + props.route?.fullname && toggleFavorite(props.route.fullname) + } + + const handleMoreOptionsClick = (event: MouseEvent) => { + event.stopPropagation() + setShowRouteOptionsCard(true) + } + return ( { directions_car } + trailing={ +
+ + +
+ } /> ) } @@ -130,31 +212,34 @@ type RouteCardProps = { route?: Route; } -const RouteCard: VoidComponent = (props) => { +const RouteCard: Component = (props) => { const route = () => props.route + const navigateToRouteActivity = () => { + location.href = `/${route()?.dongle_id}/${route()?.fullname?.slice(17)}` + } + return ( - -
-
- } - > - - -
+
) } diff --git a/src/components/RouteOptions.tsx b/src/components/RouteOptions.tsx new file mode 100644 index 00000000..bde8825d --- /dev/null +++ b/src/components/RouteOptions.tsx @@ -0,0 +1,68 @@ +import { createSignal, createEffect, type Component } from 'solid-js' +import type { Route } from '~/types' + +type RouteOptionsProps = { + route?: Route; +} + +const RouteOptions: Component = (props) => { + const [isPreservedChecked, setIsPreservedChecked] = createSignal(true) + const [isPublicAccessChecked, setIsPublicAccessChecked] = createSignal(false) + + const [routeId, setRouteId] = createSignal() + + createEffect(() => { + const routeFullName = props?.route?.fullname + setRouteId(routeFullName?.split('|')[1]) + }) + + return ( +
+
+
+ file_copy + Route ID +
+
+ share + Share +
+
+
+ Route ID: {routeId()} +
+
+
+ Preserved + +
+
+ Public Access + +
+
+
+
+ View in useradmin + + keyboard_arrow_right + +
+
+ Upload Options + + cloud_upload + +
+
+
+ ) +} + +export default RouteOptions diff --git a/src/components/RouteStaticMap.tsx b/src/components/RouteStaticMap.tsx index 1a010bea..1185e294 100644 --- a/src/components/RouteStaticMap.tsx +++ b/src/components/RouteStaticMap.tsx @@ -83,7 +83,7 @@ const RouteStaticMap: VoidComponent = (props) => { diff --git a/src/index.css b/src/index.css index c9915dc5..c4c1c4b8 100644 --- a/src/index.css +++ b/src/index.css @@ -222,4 +222,57 @@ .custom-card:hover { background-color: var(--color-surface-container); } + + .custom-switch { + position: relative; + display: inline-block; + width: 45px; + height: 28px; + } + + .custom-switch input { + opacity: 0; + width: 0; + height: 0; + } + + .custom-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #8d8d8d; + transition: .4s; + } + + .custom-slider:before { + position: absolute; + content: ""; + height: 100%; + width: 60%; + background-color: #fff; + transition: .4s; + } + + .custom-switch input:checked + .custom-slider { + background-color: #32CD32; + } + + .custom-switch input:focus + .custom-slider { + box-shadow: 0 0 1px #32CD32; + } + + .custom-switch input:checked + .custom-slider:before { + transform: translateX(26px); + } + + .custom-slider.round { + border-radius: 34px; + } + + .custom-slider.round:before { + border-radius: 50%; + } } From d6f85fdf84fd5ab6724d5f494017fffa44af04bd Mon Sep 17 00:00:00 2001 From: pir8bay Date: Sat, 6 Jul 2024 13:29:03 +0300 Subject: [PATCH 05/17] fixed improper route id in route options --- src/components/RouteCard.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 2ac9b951..4adcac21 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -13,13 +13,10 @@ import type { Route, RouteSegments } from '~/types' import { reverseGeocode } from '~/map' -type RouteOptionsProps = { - route?: Route; -} - const [showRouteOptionsCard, setShowRouteOptionsCard] = createSignal(false) +const [currentCardRoute, setCurrentCardRoute] = createSignal(undefined) -const RouteOptionsCard: Component = (props) => { +const RouteOptionsCard: Component = () => { const [isMdOrLarger, setIsMdOrLarger] = createSignal(false) // listen isMdOrLarger @@ -49,7 +46,7 @@ const RouteOptionsCard: Component = (props) => { 'transform translate-y-full': !isMdOrLarger() && !showRouteOptionsCard(), }}>
event.stopPropagation()}> - +
@@ -89,6 +86,7 @@ const RouteHeader = (props: { route?: RouteSegments }) => { const handleMoreOptionsClick = (event: MouseEvent) => { event.stopPropagation() setShowRouteOptionsCard(true) + setCurrentCardRoute(props.route) } return ( @@ -221,7 +219,7 @@ const RouteCard: Component = (props) => { return (
- +
} From 355085f70b2ac90fa0d9bbace0bcbf46fa6cd443 Mon Sep 17 00:00:00 2001 From: pir8bay Date: Sat, 6 Jul 2024 17:24:32 +0300 Subject: [PATCH 06/17] removed favorites and options from inital route card redesign --- src/components/RouteCard.tsx | 82 +-------------------------------- src/components/RouteOptions.tsx | 68 --------------------------- src/index.css | 53 --------------------- 3 files changed, 1 insertion(+), 202 deletions(-) delete mode 100644 src/components/RouteOptions.tsx diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 4adcac21..36d53bfb 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -1,10 +1,9 @@ -import { createSignal, createEffect, Suspense, Show, type Component } from 'solid-js' +import { createSignal, createEffect, Suspense, type Component } from 'solid-js' import dayjs from 'dayjs' import Avatar from '~/components/material/Avatar' import { CardContent, CardHeader } from '~/components/material/Card' import Icon from '~/components/material/Icon' -import RouteOptions from '~/components/RouteOptions' import RouteStaticMap from '~/components/RouteStaticMap' import RouteStatistics from '~/components/RouteStatistics' import Timeline from './Timeline' @@ -13,48 +12,6 @@ import type { Route, RouteSegments } from '~/types' import { reverseGeocode } from '~/map' -const [showRouteOptionsCard, setShowRouteOptionsCard] = createSignal(false) -const [currentCardRoute, setCurrentCardRoute] = createSignal(undefined) - -const RouteOptionsCard: Component = () => { - const [isMdOrLarger, setIsMdOrLarger] = createSignal(false) - - // listen isMdOrLarger - createEffect(() => { - const updateSize = () => { - setIsMdOrLarger(window.innerWidth >= 768) - } - window.addEventListener('resize', updateSize) - // Initial check - updateSize() - - return () => window.removeEventListener('resize', updateSize) - }) - - const stopPropagation = (event: MouseEvent) => { - event.stopPropagation() - - setShowRouteOptionsCard(false) - } - - return ( - -
-
event.stopPropagation()}> - -
-
-
- ) -} - -type FavoriteRoutes = string[] - const RouteHeader = (props: { route?: RouteSegments }) => { const startTime = () => props?.route?.segment_start_times ? dayjs(props.route.segment_start_times[0]) : null @@ -63,32 +20,6 @@ const RouteHeader = (props: { route?: RouteSegments }) => { const headline = () => startTime()?.format('ddd, MMM D, YYYY') const subhead = () => `${startTime()?.format('h:mm A')} to ${endTime()?.format('h:mm A')}` - const [isFavorite, setIsFavorite] = createSignal(false) - - const toggleFavorite = (routeName: string) => { - let favorites: FavoriteRoutes = JSON.parse(localStorage.getItem('favoriteRoutes') || '[]') as FavoriteRoutes - const isFavorite = favorites.includes(routeName) - favorites = isFavorite ? favorites.filter(name => name !== routeName) : [...favorites, routeName] - localStorage.setItem('favoriteRoutes', JSON.stringify(favorites)) - setIsFavorite(!isFavorite) - } - - createEffect(() => { - const favorites: FavoriteRoutes = JSON.parse(localStorage.getItem('favoriteRoutes') || '[]') as FavoriteRoutes - setIsFavorite(favorites.includes(props.route?.fullname ?? '')) - }) - - const handleLikeClick = (event: MouseEvent) => { - event.stopPropagation() - props.route?.fullname && toggleFavorite(props.route.fullname) - } - - const handleMoreOptionsClick = (event: MouseEvent) => { - event.stopPropagation() - setShowRouteOptionsCard(true) - setCurrentCardRoute(props.route) - } - return ( { directions_car } - trailing={ -
- - -
- } /> ) } @@ -219,7 +140,6 @@ const RouteCard: Component = (props) => { return (
-
} diff --git a/src/components/RouteOptions.tsx b/src/components/RouteOptions.tsx deleted file mode 100644 index bde8825d..00000000 --- a/src/components/RouteOptions.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { createSignal, createEffect, type Component } from 'solid-js' -import type { Route } from '~/types' - -type RouteOptionsProps = { - route?: Route; -} - -const RouteOptions: Component = (props) => { - const [isPreservedChecked, setIsPreservedChecked] = createSignal(true) - const [isPublicAccessChecked, setIsPublicAccessChecked] = createSignal(false) - - const [routeId, setRouteId] = createSignal() - - createEffect(() => { - const routeFullName = props?.route?.fullname - setRouteId(routeFullName?.split('|')[1]) - }) - - return ( -
-
-
- file_copy - Route ID -
-
- share - Share -
-
-
- Route ID: {routeId()} -
-
-
- Preserved - -
-
- Public Access - -
-
-
-
- View in useradmin - - keyboard_arrow_right - -
-
- Upload Options - - cloud_upload - -
-
-
- ) -} - -export default RouteOptions diff --git a/src/index.css b/src/index.css index c4c1c4b8..c9915dc5 100644 --- a/src/index.css +++ b/src/index.css @@ -222,57 +222,4 @@ .custom-card:hover { background-color: var(--color-surface-container); } - - .custom-switch { - position: relative; - display: inline-block; - width: 45px; - height: 28px; - } - - .custom-switch input { - opacity: 0; - width: 0; - height: 0; - } - - .custom-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #8d8d8d; - transition: .4s; - } - - .custom-slider:before { - position: absolute; - content: ""; - height: 100%; - width: 60%; - background-color: #fff; - transition: .4s; - } - - .custom-switch input:checked + .custom-slider { - background-color: #32CD32; - } - - .custom-switch input:focus + .custom-slider { - box-shadow: 0 0 1px #32CD32; - } - - .custom-switch input:checked + .custom-slider:before { - transform: translateX(26px); - } - - .custom-slider.round { - border-radius: 34px; - } - - .custom-slider.round:before { - border-radius: 50%; - } } From 4918550082207d6ad060b78f3d2390a14547cc4d Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Sun, 7 Jul 2024 20:20:47 +0100 Subject: [PATCH 07/17] bump bun to v1.1.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c43a56b1..9fd663e0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "pre-commit": "bun lint", "test": "vitest run" }, - "packageManager": "bun@1.1.13", + "packageManager": "bun@1.1.18", "type": "module", "devDependencies": { "@solidjs/testing-library": "^0.8.8", From 6f71332fa241e392565cdefcd98372ee546dca63 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Sun, 7 Jul 2024 20:26:03 +0100 Subject: [PATCH 08/17] lint: use cache for perf improvement Reduces from ~8s to ~2s --- eslint.config.js | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 1ac023f4..3fc385bf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,7 +7,10 @@ import solid from 'eslint-plugin-solid/configs/typescript.js' import stylistic from '@stylistic/eslint-plugin' export default [ - { languageOptions: { globals: globals.browser } }, + { + languageOptions: { globals: globals.browser }, + ignores: ['.github', '.husky', '.vscode', 'node_modules', 'dist'], + }, js.configs.recommended, ...ts.configs.recommendedTypeChecked, { @@ -46,6 +49,5 @@ export default [ '@stylistic/quotes': ['error', 'single', { avoidEscape: true }], '@stylistic/semi': ['error', 'never'], }, - ignores: ['node_modules', 'dist'], }, ] diff --git a/package.json b/package.json index 9fd663e0..39ed0974 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build": "vite build", "dev": "vite", "serve": "vite prefix", - "lint": "eslint", + "lint": "eslint --cache --cache-location node_modules/.eslintcache", "prepare": "husky", "pre-commit": "bun lint", "test": "vitest run" From 72d634e587ed25a63787c75f15ea3ebf23db672a Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Mon, 8 Jul 2024 22:53:37 +0100 Subject: [PATCH 09/17] Dashboard: default to last selected device (#55) --- src/pages/dashboard/Dashboard.tsx | 33 ++++++++++++------- src/pages/dashboard/components/DeviceList.tsx | 6 +++- src/utils/storage.ts | 19 +++++++++++ 3 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 src/utils/storage.ts diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index 4e84bfab..1fd638f4 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -8,8 +8,8 @@ import { Show, Switch, } from 'solid-js' -import type { VoidComponent } from 'solid-js' -import { Navigate, useLocation } from '@solidjs/router' +import type { Component } from 'solid-js' +import { Navigate, type RouteSectionProps, useLocation } from '@solidjs/router' import { getDevices } from '~/api/devices' import { getProfile } from '~/api/profile' @@ -23,6 +23,7 @@ import TopAppBar from '~/components/material/TopAppBar' import DeviceList from './components/DeviceList' import DeviceActivity from './activities/DeviceActivity' import RouteActivity from './activities/RouteActivity' +import storage from '~/utils/storage' type DashboardState = { drawer: Accessor @@ -57,7 +58,7 @@ const DashboardDrawer = (props: { ) } -const DashboardLayout: VoidComponent = () => { +const DashboardLayout: Component = () => { const location = useLocation() const pathParts = () => location.pathname.split('/').slice(1).filter(Boolean) @@ -72,6 +73,15 @@ const DashboardLayout: VoidComponent = () => { const [devices] = createResource(getDevices) const [profile] = createResource(getProfile) + const getDefaultDongleId = () => { + // Do not redirect if dongle ID already selected + if (dongleId()) return undefined + + const lastSelectedDongleId = storage.getItem('lastSelectedDongleId') + if (devices()?.some((device) => device.dongle_id === lastSelectedDongleId)) return lastSelectedDongleId + return devices()?.[0]?.dongle_id + } + return ( { > - menu} - > - No device - - + menu} + > + No device + } > @@ -100,8 +108,9 @@ const DashboardLayout: VoidComponent = () => { - - + {(defaultDongleId) => ( + + )} diff --git a/src/pages/dashboard/components/DeviceList.tsx b/src/pages/dashboard/components/DeviceList.tsx index 14cdea95..c08eda35 100644 --- a/src/pages/dashboard/components/DeviceList.tsx +++ b/src/pages/dashboard/components/DeviceList.tsx @@ -7,6 +7,7 @@ import Icon from '~/components/material/Icon' import List, { ListItem, ListItemContent } from '~/components/material/List' import type { Device } from '~/types' import { getDeviceName } from '~/utils/device' +import storage from '~/utils/storage' import { DashboardContext } from '../Dashboard' @@ -24,7 +25,10 @@ const DeviceList: VoidComponent = (props) => { {(device) => { const isSelected = () => location.pathname.includes(device.dongle_id) - const onClick = () => setDrawer(false) + const onClick = () => { + setDrawer(false) + storage.setItem('lastSelectedDongleId', device.dongle_id) + } return ( Date: Thu, 11 Jul 2024 14:42:51 -0700 Subject: [PATCH 10/17] 1.0 roadmap (#61) --- README.md | 63 +++++++++++++++++++++++++------------------------------ 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index b2531b52..fc1cdcdf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # connect -This is an experimental rewrite of [comma connect](https://github.com/commaai/connect), the web (and mobile) experience for [openpilot](https://github.com/commaai/openpilot). +This is a rewrite of [comma connect](https://github.com/commaai/connect), the web (and mobile) experience for [openpilot](https://github.com/commaai/openpilot). -Try out new-connect at https://new-connect.connect-d5y.pages.dev. +Try it out at https://new-connect.connect-d5y.pages.dev. ## Development @@ -14,36 +14,29 @@ Try out new-connect at https://new-connect.connect-d5y.pages.dev. Join the `#dev-connect-web` channel on our [Discord](https://discord.comma.ai). -A few constraints: -- 5k line limit -- 500KB bundle size limit -- 1m timeout for all CI; a fast development environment is a delightful one - -## Features - -These are the minimum features for parity with connect. - -Drives -- [x] list -- [x] show map -- [x] play qcams -- [x] engagement timeline -- [ ] file uploads - -Map -- [ ] show user and car location - -Misc -- [x] demo mode -- [ ] snapshot -- [ ] comma prime sign up + management -- [ ] pairing to a new device -- [ ] PWA: splash, icon, offline mode, etc. - -And some eventual features beyond connect's current feature set: -- [ ] SSH console -- [ ] sentry mode -- [ ] dashcam clips -- [ ] manage openpilot settings -- [ ] replace all of useradmin.comma.ai -- [ ] car mangement: lock doors, EV charge status, etc. +connect has a demo mode, so no special comma device is needed to develop connect. + +A few constraints to keep connect light and the dev environment fun: +* 5k line limit +* 500KB bundle size limit +* 1m timeout for all CI + +References: +* [API docs](https://api.comma.ai) +* [openpilot docs](https://docs.comma.ai) +* [Discord](https://discord.comma.ai) +* [Bounties](https://comma.ai/bounties) + +## Roadmap + +The first goal is to replace current connect and get this shipped to https://connect.comma.ai. + +[This project](https://github.com/orgs/commaai/projects/32) tracks that progress. Most of the issues there are [paid bounties](https://comma.ai/bounties). + +Once we've shipped v1, next up will be: +* [Sentry mode](https://www.youtube.com/watch?v=laO0RzsDzfU) +* SSH console for openpilot developers +* Replace snapshot with a live stream +* openpilot clips, like this [community tool](https://github.com/nelsonjchen/op-replay-clipper) +* Manage the settings on your comma 3X +* Car mangement: lock doors, EV charge status, etc. From bd62fd7f777a20420a82b75ea99d9dac364f26a8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 11 Jul 2024 14:48:50 -0700 Subject: [PATCH 11/17] it's a milestone now --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc1cdcdf..426f7e2b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ References: The first goal is to replace current connect and get this shipped to https://connect.comma.ai. -[This project](https://github.com/orgs/commaai/projects/32) tracks that progress. Most of the issues there are [paid bounties](https://comma.ai/bounties). +[This milestone](https://github.com/commaai/new-connect/milestone/1) tracks that progress. Most of the issues there are [paid bounties](https://comma.ai/bounties). Once we've shipped v1, next up will be: * [Sentry mode](https://www.youtube.com/watch?v=laO0RzsDzfU) From ca1ec0134fdcb20df5d08d4f7cdeb2f54cd8cb29 Mon Sep 17 00:00:00 2001 From: pir8bay Date: Fri, 12 Jul 2024 15:38:55 +0300 Subject: [PATCH 12/17] removed use of segment times and friends --- src/components/RouteCard.tsx | 4 ++-- src/types.d.ts | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 36d53bfb..08bae68b 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -14,8 +14,8 @@ import { reverseGeocode } from '~/map' const RouteHeader = (props: { route?: RouteSegments }) => { - const startTime = () => props?.route?.segment_start_times ? dayjs(props.route.segment_start_times[0]) : null - const endTime = () => props?.route?.segment_end_times ? dayjs(props.route.segment_end_times.at(-1)) : null + const startTime = () => props?.route?.start_time_utc_millis ? dayjs(props.route.start_time_utc_millis) : null + const endTime = () => props?.route?.end_time_utc_millis ? dayjs(props.route.end_time_utc_millis) : null const headline = () => startTime()?.format('ddd, MMM D, YYYY') const subhead = () => `${startTime()?.format('h:mm A')} to ${endTime()?.format('h:mm A')}` diff --git a/src/types.d.ts b/src/types.d.ts index 6dd235d0..8d69f45d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -92,9 +92,6 @@ export interface RouteShareSignature extends Record { export interface RouteSegments extends Route { end_time_utc_millis?: number is_preserved?: boolean - segment_end_times?: number[] - segment_numbers?: number[] - segment_start_times?: number[] share_exp?: RouteShareSignature['exp'] share_sig?: RouteShareSignature['sig'] start_time_utc_millis?: number From 83431ec091a5b1c6dff047d98d3787df97d5b9b3 Mon Sep 17 00:00:00 2001 From: pir8bay Date: Fri, 12 Jul 2024 16:02:13 +0300 Subject: [PATCH 13/17] handle RouteCard time display --- src/components/RouteCard.tsx | 15 +++++++++++++-- src/types.d.ts | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 08bae68b..de681526 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -17,8 +17,19 @@ const RouteHeader = (props: { route?: RouteSegments }) => { const startTime = () => props?.route?.start_time_utc_millis ? dayjs(props.route.start_time_utc_millis) : null const endTime = () => props?.route?.end_time_utc_millis ? dayjs(props.route.end_time_utc_millis) : null - const headline = () => startTime()?.format('ddd, MMM D, YYYY') - const subhead = () => `${startTime()?.format('h:mm A')} to ${endTime()?.format('h:mm A')}` + const headline = () => { + if (!startTime() && !endTime()) { + return 'No time info' + } + return startTime()?.format('ddd, MMM D, YYYY') || 'No Time Info' + } + + const subhead = () => { + if (!startTime() && !endTime()) { + return '' + } + return `${startTime()?.format('h:mm A') || 'No start time'} to ${endTime()?.format('h:mm A') || 'No end time'}` + } return ( Date: Sat, 13 Jul 2024 04:24:26 +0300 Subject: [PATCH 14/17] fixed linting error --- src/pages/dashboard/components/RouteList.tsx | 21 ++++++++++++++++---- src/types.d.ts | 11 ++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 78d0cf40..f47d1371 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -26,12 +26,25 @@ const pages: Promise[] = [] const RouteList: VoidComponent = (props) => { const endpoint = () => `/v1/devices/${props.dongleId}/routes_segments?limit=${PAGE_SIZE}` + const getKey = (previousPageData?: RouteSegments[]): string | undefined => { - if (!previousPageData) return endpoint() - if (previousPageData.length === 0) return undefined - const lastSegmentEndTime = previousPageData.at(-1)!.segment_start_times.at(-1)! - return `${endpoint()}&end=${lastSegmentEndTime - 1}` + if (!previousPageData || previousPageData.length === 0) { + return undefined + } + + const lastSegment = previousPageData[previousPageData.length - 1] + if (!lastSegment || !lastSegment.segment_start_times || lastSegment.segment_start_times.length === 0) { + return endpoint() + } + + const lastSegmentEndTime = lastSegment.segment_start_times[lastSegment.segment_start_times.length - 1] + if (lastSegmentEndTime) { + return `${endpoint()}&end=${lastSegmentEndTime - 1}` + } else { + return endpoint() + } } + const getPage = (page: number): Promise => { if (!pages[page]) { // eslint-disable-next-line no-async-promise-executor diff --git a/src/types.d.ts b/src/types.d.ts index 423ef4f6..e219c997 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -84,6 +84,17 @@ export interface Route { vin?: string } +export interface RouteSegments extends Route { + end_time_utc_millis?: number + is_preserved?: boolean + segment_end_times?: number[] + segment_numbers?: number[] + segment_start_times?: number[] + share_exp?: RouteShareSignature['exp'] + share_sig?: RouteShareSignature['sig'] + start_time_utc_millis?: number +} + export interface RouteShareSignature extends Record { exp: string sig: string From 42432a1d4042a857eb29f44a47a366880493cef6 Mon Sep 17 00:00:00 2001 From: pir8bay Date: Sat, 13 Jul 2024 04:28:34 +0300 Subject: [PATCH 15/17] bug fix --- src/pages/dashboard/components/RouteList.tsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index f47d1371..86c9125d 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -28,21 +28,11 @@ const RouteList: VoidComponent = (props) => { const endpoint = () => `/v1/devices/${props.dongleId}/routes_segments?limit=${PAGE_SIZE}` const getKey = (previousPageData?: RouteSegments[]): string | undefined => { - if (!previousPageData || previousPageData.length === 0) { - return undefined - } - - const lastSegment = previousPageData[previousPageData.length - 1] - if (!lastSegment || !lastSegment.segment_start_times || lastSegment.segment_start_times.length === 0) { - return endpoint() - } - - const lastSegmentEndTime = lastSegment.segment_start_times[lastSegment.segment_start_times.length - 1] - if (lastSegmentEndTime) { - return `${endpoint()}&end=${lastSegmentEndTime - 1}` - } else { - return endpoint() - } + if (!previousPageData) return endpoint() + if (previousPageData.length === 0) return undefined + const lastSegment = previousPageData.at(-1)! + const lastSegmentEndTime = lastSegment.segment_start_times ? lastSegment.segment_start_times.at(-1) : undefined + return lastSegmentEndTime ? `${endpoint()}&end=${lastSegmentEndTime - 1}` : undefined } const getPage = (page: number): Promise => { From 0e2f4d185e3cf53ca56b2999eab0e606351baa91 Mon Sep 17 00:00:00 2001 From: pir8bay Date: Mon, 15 Jul 2024 02:34:03 +0300 Subject: [PATCH 16/17] resync --- src/components/RouteCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index de681526..0ebffbe9 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -21,7 +21,7 @@ const RouteHeader = (props: { route?: RouteSegments }) => { if (!startTime() && !endTime()) { return 'No time info' } - return startTime()?.format('ddd, MMM D, YYYY') || 'No Time Info' + return startTime()?.format('ddd, MMM D, YYYY') || 'No Time Info!' } const subhead = () => { From f84c6bba46cda1cfb943b6d11e05606d7146219a Mon Sep 17 00:00:00 2001 From: pir8bay Date: Mon, 15 Jul 2024 03:19:00 +0300 Subject: [PATCH 17/17] fix ts error --- src/pages/dashboard/components/RouteList.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 31349251..e56fba71 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -30,8 +30,12 @@ const RouteList: VoidComponent = (props) => { const getKey = (previousPageData?: RouteSegments[]): string | undefined => { if (!previousPageData) return endpoint() if (previousPageData.length === 0) return undefined - const lastSegmentEndTime = previousPageData.at(-1)!.end_time_utc_millis - return `${endpoint()}&end=${lastSegmentEndTime - 1}` + const lastSegment = previousPageData.at(-1) + if (lastSegment && lastSegment.end_time_utc_millis !== undefined) { + const lastSegmentEndTime = lastSegment.end_time_utc_millis + return `${endpoint()}&end=${lastSegmentEndTime - 1}` + } + return undefined } const getPage = (page: number): Promise => {