From ca0fdf7db49d32d2496493f715e527db05751148 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Tue, 9 Jul 2024 17:06:29 -0700 Subject: [PATCH 01/20] UI looks good but data NOT linked --- src/components/RouteSorter.tsx | 85 +++++++++++++++++++ .../dashboard/activities/DeviceActivity.tsx | 2 + 2 files changed, 87 insertions(+) create mode 100644 src/components/RouteSorter.tsx diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx new file mode 100644 index 00000000..b6180781 --- /dev/null +++ b/src/components/RouteSorter.tsx @@ -0,0 +1,85 @@ +import { createSignal, For } from 'solid-js' +import { createStore } from 'solid-js/store' + +interface SortOption { + label: string + key: string + order: 'asc' | 'desc' | null +} + +// ! Make this dynamic if light mode is ready +const GRADIENT = 'from-cyan-700 via-blue-800 to-purple-900' + +export const RouteSorter = () => { + const [sortOptions, setSortOptions] = createStore([ + { label: 'Date', key: 'date', order: 'desc' }, + { label: 'Duration', key: 'duration', order: null }, + { label: 'Miles', key: 'miles', order: null }, + { label: 'Engaged', key: 'engaged', order: null }, + { label: 'User Flags', key: 'user-flags', order: null }, + ]) + + // ? Do I need scrollPosition or this line of code? + const [setScrollPosition] = createSignal(0) + + // Handles horizontal scrolling with the mouse wheel. + const handleScroll = (e: WheelEvent) => { + const container = e.currentTarget as HTMLDivElement + container.scrollLeft += e.deltaY + setScrollPosition(container.scrollLeft) + } + + const handleClick = (clickedIndex: number) => { + setSortOptions(option => option.key === sortOptions[clickedIndex].key, 'order', current => + current === 'desc' ? 'asc' : 'desc', + ) + setSortOptions(option => option.key !== sortOptions[clickedIndex].key, 'order', null) + } + + return ( +
+
+ + {(option, index) => ( + + )} + +
+
+ ) +} + +export default RouteSorter diff --git a/src/pages/dashboard/activities/DeviceActivity.tsx b/src/pages/dashboard/activities/DeviceActivity.tsx index f47638f0..6ae19671 100644 --- a/src/pages/dashboard/activities/DeviceActivity.tsx +++ b/src/pages/dashboard/activities/DeviceActivity.tsx @@ -6,6 +6,7 @@ import { getDevice } from '~/api/devices' import IconButton from '~/components/material/IconButton' import TopAppBar from '~/components/material/TopAppBar' import DeviceStatistics from '~/components/DeviceStatistics' +import RouteSorter from '../../../components/RouteSorter' import { getDeviceName } from '~/utils/device' import RouteList from '../components/RouteList' @@ -35,6 +36,7 @@ const DeviceActivity: VoidComponent = (props) => {
Routes +
From 3bf268c66a3acfb3769b7eba046dfd9dc9cec910 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Tue, 9 Jul 2024 23:55:10 -0700 Subject: [PATCH 02/20] Its functioning - Still needs work --- src/api/route.ts | 20 ++- src/components/RouteSorter.tsx | 34 +++-- .../dashboard/activities/DeviceActivity.tsx | 2 - src/pages/dashboard/components/RouteList.tsx | 123 +++++++++--------- src/utils/sorting.ts | 46 +++++++ 5 files changed, 148 insertions(+), 77 deletions(-) create mode 100644 src/utils/sorting.ts diff --git a/src/api/route.ts b/src/api/route.ts index 60bd0852..1a467f99 100644 --- a/src/api/route.ts +++ b/src/api/route.ts @@ -1,6 +1,6 @@ import { fetcher } from '.' import { BASE_URL } from './config' -import type { Device, Route, RouteShareSignature } from '~/types' +import type { Device, Route, RouteShareSignature, RouteSegments } from '~/types' export class RouteName { // dongle ID date str @@ -49,3 +49,21 @@ export const getQCameraStreamUrl = (routeName: Route['fullname']): Promise createQCameraStreamUrl(routeName, signature), ) + +export const fetchRoutes = async ({ + dongleId, + page, + pageSize, +}: { + dongleId: string + page: number + pageSize: number +}): Promise => { + const endpoint = `/v1/devices/${dongleId}/routes_segments` + const params = new URLSearchParams({ + limit: pageSize.toString(), + offset: ((page - 1) * pageSize).toString(), + }) + + return fetcher(`${endpoint}?${params.toString()}`) +} diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index b6180781..48d1bf10 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -1,7 +1,8 @@ -import { createSignal, For } from 'solid-js' +import { For } from 'solid-js' import { createStore } from 'solid-js/store' +import type { Component } from 'solid-js' -interface SortOption { +export interface SortOption { label: string key: string order: 'asc' | 'desc' | null @@ -10,30 +11,41 @@ interface SortOption { // ! Make this dynamic if light mode is ready const GRADIENT = 'from-cyan-700 via-blue-800 to-purple-900' -export const RouteSorter = () => { +interface RouteSorterProps { + onSortChange: (key: string, order: 'asc' | 'desc' | null) => void +} + +export const RouteSorter: Component = (props) => { const [sortOptions, setSortOptions] = createStore([ { label: 'Date', key: 'date', order: 'desc' }, { label: 'Duration', key: 'duration', order: null }, { label: 'Miles', key: 'miles', order: null }, { label: 'Engaged', key: 'engaged', order: null }, - { label: 'User Flags', key: 'user-flags', order: null }, + { label: 'User Flags', key: 'userFlags', order: null }, ]) - // ? Do I need scrollPosition or this line of code? - const [setScrollPosition] = createSignal(0) - // Handles horizontal scrolling with the mouse wheel. const handleScroll = (e: WheelEvent) => { const container = e.currentTarget as HTMLDivElement container.scrollLeft += e.deltaY - setScrollPosition(container.scrollLeft) } const handleClick = (clickedIndex: number) => { - setSortOptions(option => option.key === sortOptions[clickedIndex].key, 'order', current => - current === 'desc' ? 'asc' : 'desc', + const clickedOption = sortOptions[clickedIndex] + const newOrder = clickedOption.order === 'desc' ? 'asc' : 'desc' + + setSortOptions( + option => option.key === clickedOption.key, + 'order', + newOrder, + ) + setSortOptions( + option => option.key !== clickedOption.key, + 'order', + null, ) - setSortOptions(option => option.key !== sortOptions[clickedIndex].key, 'order', null) + + props.onSortChange(clickedOption.key, newOrder) } return ( diff --git a/src/pages/dashboard/activities/DeviceActivity.tsx b/src/pages/dashboard/activities/DeviceActivity.tsx index 6ae19671..f47638f0 100644 --- a/src/pages/dashboard/activities/DeviceActivity.tsx +++ b/src/pages/dashboard/activities/DeviceActivity.tsx @@ -6,7 +6,6 @@ import { getDevice } from '~/api/devices' import IconButton from '~/components/material/IconButton' import TopAppBar from '~/components/material/TopAppBar' import DeviceStatistics from '~/components/DeviceStatistics' -import RouteSorter from '../../../components/RouteSorter' import { getDeviceName } from '~/utils/device' import RouteList from '../components/RouteList' @@ -36,7 +35,6 @@ const DeviceActivity: VoidComponent = (props) => {
Routes -
diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 78d0cf40..ab7db1d6 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -1,89 +1,86 @@ -/* eslint-disable @typescript-eslint/no-misused-promises */ import { createEffect, createResource, createSignal, For, - Suspense, + onCleanup, } from 'solid-js' -import type { VoidComponent } from 'solid-js' -import clsx from 'clsx' - -import type { RouteSegments } from '~/types' - +import type { Component } from 'solid-js' +import { RouteSegments } from '~/types' +import { SortOption, sortRoutes } from '~/utils/sorting' +import { fetchRoutes } from '~/api/route' import RouteCard from '~/components/RouteCard' -import { fetcher } from '~/api' -import Button from '~/components/material/Button' +import RouteSorter from '~/components/RouteSorter' -const PAGE_SIZE = 3 +const PAGE_SIZE = 10 -type RouteListProps = { - class?: string +interface RouteListProps { dongleId: string } -const pages: Promise[] = [] +const RouteList: Component = (props) => { + const [sortOption, setSortOption] = createSignal({ key: 'date', order: 'desc' }) + const [page, setPage] = createSignal(1) + const [allRoutes, setAllRoutes] = createSignal([]) + const [sortedRoutes, setSortedRoutes] = createSignal([]) -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}` - } - const getPage = (page: number): Promise => { - if (!pages[page]) { - // eslint-disable-next-line no-async-promise-executor - pages[page] = new Promise(async (resolve) => { - const previousPageData = page > 0 ? await getPage(page - 1) : undefined - const key = getKey(previousPageData) - resolve(key ? fetcher(key) : []) - }) + const [routesData, { refetch }] = createResource( + () => ({ dongleId: props.dongleId, page: page(), pageSize: PAGE_SIZE }), + fetchRoutes, + ) + + createEffect(() => { + const newRoutes = routesData() + if (newRoutes) { + setAllRoutes(prev => [...prev, ...newRoutes]) + } + }) + + createEffect(async () => { + const routes = allRoutes() + if (routes.length > 0) { + const sorted = await sortRoutes(routes, sortOption()) + setSortedRoutes(sorted) } - return pages[page] + }) + + // ! Fix this + const handleSortChange = (key: string, order: 'asc' | 'desc' | null) => { + setSortOption({ key, order: order || 'desc' }) + setPage(1) + setAllRoutes([]) + void refetch() } + let bottomRef: HTMLDivElement | undefined + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !routesData.loading) { + setPage(p => p + 1) + } + }, + { rootMargin: '200px' }, + ) + createEffect(() => { - if (props.dongleId) { - pages.length = 0 - setSize(1) + if (bottomRef) { + observer.observe(bottomRef) + } + return () => { + if (bottomRef) observer.unobserve(bottomRef) } }) - const [size, setSize] = createSignal(1) - const onLoadMore = () => setSize(size() + 1) - const pageNumbers = () => Array.from(Array(size()).keys()) + onCleanup(() => observer.disconnect()) return ( -
- - {(i) => { - const [routes] = createResource(() => i, getPage) - return ( - -
-
-
- - } - > - - {(route) => } - - - ) - }} +
+ + + {(route: RouteSegments) => } -
- +
+ {routesData.loading &&
Loading...
}
) diff --git a/src/utils/sorting.ts b/src/utils/sorting.ts new file mode 100644 index 00000000..48994670 --- /dev/null +++ b/src/utils/sorting.ts @@ -0,0 +1,46 @@ +import { RouteSegments } from '~/types' +import { getTimelineStatistics, TimelineStatistics } from '~/api/derived' + +export type SortKey = 'date' | 'miles' | 'duration' | 'engaged' | 'userFlags' +export type SortOrder = 'asc' | 'desc' + +export interface SortOption { + key: SortKey + order: SortOrder +} + +export const sortRoutes = async (routes: RouteSegments[], option: SortOption): Promise => { + const { key, order } = option + + // Fetch timeline statistics for all routes + const statisticsPromises = routes.map(route => getTimelineStatistics(route)) + const statistics = await Promise.all(statisticsPromises) + + // Create a map of route to its statistics for easy lookup + const statsMap = new Map() + routes.forEach((route, index) => statsMap.set(route, statistics[index])) + + return [...routes].sort((a, b) => { + let comparison = 0 + + switch (key) { + case 'date': + comparison = b.start_time_utc_millis - a.start_time_utc_millis // Most recent first + break + case 'miles': + comparison = (b.length || 0) - (a.length || 0) + break + case 'duration': + comparison = statsMap.get(b)!.duration - statsMap.get(a)!.duration + break + case 'engaged': + comparison = statsMap.get(b)!.engagedDuration - statsMap.get(a)!.engagedDuration + break + case 'userFlags': + comparison = statsMap.get(b)!.userFlags - statsMap.get(a)!.userFlags + break + } + + return order === 'asc' ? comparison : -comparison + }) +} From 020f4a6d85426ae569550e1c35bc7179e80fbba3 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Wed, 10 Jul 2024 00:00:51 -0700 Subject: [PATCH 03/20] Switch sort arrows to reflect asc dsc --- src/components/RouteSorter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index 48d1bf10..68952ebe 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -83,7 +83,7 @@ export const RouteSorter: Component = (props) => { {option.order && ( - {option.order === 'asc' ? '↑' : '↓'} + {option.order === 'asc' ? '↓' : '↑'} )} From 05cf3c779addf185fe159a51d7f691b5d78cd786 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Wed, 10 Jul 2024 09:41:52 -0700 Subject: [PATCH 04/20] UI still functions - cleaner code - but still needs work --- src/components/RouteSorter.tsx | 58 ++++++++------------ src/pages/dashboard/components/RouteList.tsx | 29 ++++++---- src/utils/sorting.ts | 3 +- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index 68952ebe..ce774b25 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -1,50 +1,38 @@ import { For } from 'solid-js' import { createStore } from 'solid-js/store' import type { Component } from 'solid-js' +import { SortKey, SortOption, SortOrder } from '~/utils/sorting' -export interface SortOption { - label: string - key: string - order: 'asc' | 'desc' | null -} - -// ! Make this dynamic if light mode is ready const GRADIENT = 'from-cyan-700 via-blue-800 to-purple-900' interface RouteSorterProps { - onSortChange: (key: string, order: 'asc' | 'desc' | null) => void + onSortChange: (key: SortKey, order: SortOrder) => void + currentSort: SortOption } export const RouteSorter: Component = (props) => { - const [sortOptions, setSortOptions] = createStore([ - { label: 'Date', key: 'date', order: 'desc' }, + const [sortOptions] = createStore([ + { label: 'Date', key: 'date', order: null }, { label: 'Duration', key: 'duration', order: null }, { label: 'Miles', key: 'miles', order: null }, { label: 'Engaged', key: 'engaged', order: null }, { label: 'User Flags', key: 'userFlags', order: null }, ]) - // Handles horizontal scrolling with the mouse wheel. const handleScroll = (e: WheelEvent) => { const container = e.currentTarget as HTMLDivElement container.scrollLeft += e.deltaY } - const handleClick = (clickedIndex: number) => { - const clickedOption = sortOptions[clickedIndex] - const newOrder = clickedOption.order === 'desc' ? 'asc' : 'desc' - - setSortOptions( - option => option.key === clickedOption.key, - 'order', - newOrder, - ) - setSortOptions( - option => option.key !== clickedOption.key, - 'order', - null, - ) - + const handleClick = (clickedOption: SortOption) => { + let newOrder: SortOrder + if (props.currentSort.key === clickedOption.key) { + // If the same button is clicked, toggle between asc and desc + newOrder = props.currentSort.order === 'desc' ? 'asc' : 'desc' + } else { + // If a new button is clicked, always start with descending order + newOrder = 'desc' + } props.onSortChange(clickedOption.key, newOrder) } @@ -56,34 +44,34 @@ export const RouteSorter: Component = (props) => { onWheel={handleScroll} > - {(option, index) => ( + {(option) => ( diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index ab7db1d6..f5e85dcf 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -7,7 +7,7 @@ import { } from 'solid-js' import type { Component } from 'solid-js' import { RouteSegments } from '~/types' -import { SortOption, sortRoutes } from '~/utils/sorting' +import { SortOption, SortKey, sortRoutes } from '~/utils/sorting' import { fetchRoutes } from '~/api/route' import RouteCard from '~/components/RouteCard' import RouteSorter from '~/components/RouteSorter' @@ -19,7 +19,7 @@ interface RouteListProps { } const RouteList: Component = (props) => { - const [sortOption, setSortOption] = createSignal({ key: 'date', order: 'desc' }) + const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'desc' }) const [page, setPage] = createSignal(1) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) @@ -29,6 +29,7 @@ const RouteList: Component = (props) => { fetchRoutes, ) + // Effect to update allRoutes when new routesData is available createEffect(() => { const newRoutes = routesData() if (newRoutes) { @@ -36,20 +37,28 @@ const RouteList: Component = (props) => { } }) - createEffect(async () => { + // Effect to sort routes whenever allRoutes or sortOption changes + createEffect(() => { const routes = allRoutes() + const currentSortOption = sortOption() if (routes.length > 0) { - const sorted = await sortRoutes(routes, sortOption()) - setSortedRoutes(sorted) + void sortAndSetRoutes(routes, currentSortOption) } }) - // ! Fix this - const handleSortChange = (key: string, order: 'asc' | 'desc' | null) => { - setSortOption({ key, order: order || 'desc' }) + // Function to sort and set sorted routes + const sortAndSetRoutes = async (routes: RouteSegments[], currentSortOption: SortOption) => { + const sorted = await sortRoutes(routes, currentSortOption) + setSortedRoutes(sorted) + } + + // Handle sort change without returning a promise + const handleSortChange = (key: SortKey, order: 'asc' | 'desc') => { + const label = key.charAt(0).toUpperCase() + key.slice(1) // Create a label from the key + setSortOption({ label, key, order }) setPage(1) setAllRoutes([]) - void refetch() + void refetch() } let bottomRef: HTMLDivElement | undefined @@ -75,7 +84,7 @@ const RouteList: Component = (props) => { return (
- + {(route: RouteSegments) => } diff --git a/src/utils/sorting.ts b/src/utils/sorting.ts index 48994670..26e8e0b1 100644 --- a/src/utils/sorting.ts +++ b/src/utils/sorting.ts @@ -5,8 +5,9 @@ export type SortKey = 'date' | 'miles' | 'duration' | 'engaged' | 'userFlags' export type SortOrder = 'asc' | 'desc' export interface SortOption { + label: string key: SortKey - order: SortOrder + order: SortOrder | null } export const sortRoutes = async (routes: RouteSegments[], option: SortOption): Promise => { From 67646d7f235b2e2624d9094f89c12e355b2e0a14 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Wed, 10 Jul 2024 12:26:39 -0700 Subject: [PATCH 05/20] Includes console log to understand why sorting NOT desc --- src/components/RouteSorter.tsx | 3 +- src/pages/dashboard/components/RouteList.tsx | 50 ++++++++++++++------ src/types.d.ts | 4 ++ src/utils/sorting.ts | 33 ++++++++++--- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index ce774b25..db40c227 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -19,6 +19,7 @@ export const RouteSorter: Component = (props) => { { label: 'User Flags', key: 'userFlags', order: null }, ]) + // Allows mouse wheel to scroll through filters const handleScroll = (e: WheelEvent) => { const container = e.currentTarget as HTMLDivElement container.scrollLeft += e.deltaY @@ -27,7 +28,7 @@ export const RouteSorter: Component = (props) => { const handleClick = (clickedOption: SortOption) => { let newOrder: SortOrder if (props.currentSort.key === clickedOption.key) { - // If the same button is clicked, toggle between asc and desc + // If the same button is clicked, toggle the order newOrder = props.currentSort.order === 'desc' ? 'asc' : 'desc' } else { // If a new button is clicked, always start with descending order diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index f5e85dcf..64b979d9 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -1,10 +1,4 @@ -import { - createEffect, - createResource, - createSignal, - For, - onCleanup, -} from 'solid-js' +import { createEffect, createResource, createSignal, For, onCleanup } from 'solid-js' import type { Component } from 'solid-js' import { RouteSegments } from '~/types' import { SortOption, SortKey, sortRoutes } from '~/utils/sorting' @@ -29,38 +23,66 @@ const RouteList: Component = (props) => { fetchRoutes, ) - // Effect to update allRoutes when new routesData is available createEffect(() => { const newRoutes = routesData() if (newRoutes) { - setAllRoutes(prev => [...prev, ...newRoutes]) + setAllRoutes(prev => { + const uniqueNewRoutes = newRoutes.filter(newRoute => + !prev.some(existingRoute => existingRoute.start_time === newRoute.start_time), + ) + return [...prev, ...uniqueNewRoutes] + }) } }) - // Effect to sort routes whenever allRoutes or sortOption changes createEffect(() => { const routes = allRoutes() const currentSortOption = sortOption() + console.log('Current all routes:', routes.map(r => ({ + start_time: r.start_time_utc_millis, + duration: r.duration, + miles: r.length, + engaged: r.engagedDuration, + userFlags: r.userFlags, + }))) + console.log('Current sort option:', currentSortOption) if (routes.length > 0) { void sortAndSetRoutes(routes, currentSortOption) } }) - // Function to sort and set sorted routes const sortAndSetRoutes = async (routes: RouteSegments[], currentSortOption: SortOption) => { + console.log('Sorting with option:', currentSortOption) const sorted = await sortRoutes(routes, currentSortOption) + console.log('Sorted routes before setting state:', sorted.map(r => ({ + start_time: r.start_time_utc_millis, + duration: r.duration, + miles: r.length, + engaged: r.engagedDuration, + userFlags: r.userFlags, + }))) setSortedRoutes(sorted) + console.log('Routes after setting state:', sortedRoutes().map(r => ({ + start_time: r.start_time_utc_millis, + duration: r.duration, + miles: r.length, + engaged: r.engagedDuration, + userFlags: r.userFlags, + }))) } - // Handle sort change without returning a promise const handleSortChange = (key: SortKey, order: 'asc' | 'desc') => { - const label = key.charAt(0).toUpperCase() + key.slice(1) // Create a label from the key + const label = key.charAt(0).toUpperCase() + key.slice(1) setSortOption({ label, key, order }) setPage(1) - setAllRoutes([]) void refetch() } + // Add this effect to log sorted routes whenever they change + createEffect(() => { + console.log('Routes at render:', sortedRoutes().map(r => ({ start_time: r.start_time_utc_millis, create_time: r.create_time }))) + }) + let bottomRef: HTMLDivElement | undefined const observer = new IntersectionObserver( (entries) => { diff --git a/src/types.d.ts b/src/types.d.ts index cc9d76e4..c5de46d0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -96,6 +96,10 @@ export interface RouteSegments extends Route { share_exp: RouteShareSignature['exp'] share_sig: RouteShareSignature['sig'] start_time_utc_millis: number + duration?: number + engagedDuration?: number + userFlags?: number + length?: number } export interface Clip { diff --git a/src/utils/sorting.ts b/src/utils/sorting.ts index 26e8e0b1..6951403d 100644 --- a/src/utils/sorting.ts +++ b/src/utils/sorting.ts @@ -2,15 +2,16 @@ import { RouteSegments } from '~/types' import { getTimelineStatistics, TimelineStatistics } from '~/api/derived' export type SortKey = 'date' | 'miles' | 'duration' | 'engaged' | 'userFlags' -export type SortOrder = 'asc' | 'desc' +export type SortOrder = 'asc' | 'desc' | null export interface SortOption { label: string key: SortKey - order: SortOrder | null + order: SortOrder } export const sortRoutes = async (routes: RouteSegments[], option: SortOption): Promise => { + console.log('Sorting routes with option:', option) const { key, order } = option // Fetch timeline statistics for all routes @@ -21,27 +22,45 @@ export const sortRoutes = async (routes: RouteSegments[], option: SortOption): P const statsMap = new Map() routes.forEach((route, index) => statsMap.set(route, statistics[index])) - return [...routes].sort((a, b) => { + // Add all relevant data to each route object + const routesWithData = routes.map(route => ({ + ...route, + duration: statsMap.get(route)?.duration || 0, + engagedDuration: statsMap.get(route)?.engagedDuration || 0, + userFlags: statsMap.get(route)?.userFlags || 0, + })) + + const sortedRoutes = routesWithData.sort((a, b) => { let comparison = 0 switch (key) { case 'date': - comparison = b.start_time_utc_millis - a.start_time_utc_millis // Most recent first + comparison = b.start_time_utc_millis - a.start_time_utc_millis break case 'miles': comparison = (b.length || 0) - (a.length || 0) break case 'duration': - comparison = statsMap.get(b)!.duration - statsMap.get(a)!.duration + comparison = b.duration - a.duration break case 'engaged': - comparison = statsMap.get(b)!.engagedDuration - statsMap.get(a)!.engagedDuration + comparison = b.engagedDuration - a.engagedDuration break case 'userFlags': - comparison = statsMap.get(b)!.userFlags - statsMap.get(a)!.userFlags + comparison = b.userFlags - a.userFlags break } return order === 'asc' ? comparison : -comparison }) + + console.log('Sorted routes:', sortedRoutes.map(r => ({ + start_time: r.start_time_utc_millis, + duration: r.duration, + miles: r.length, + engaged: r.engagedDuration, + userFlags: r.userFlags, + }))) + + return sortedRoutes } From 424b3b06a36bc7e3614b765d4f1b9dfab9d6493f Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Wed, 10 Jul 2024 16:27:43 -0700 Subject: [PATCH 06/20] Added hover effect for filter btns --- src/components/RouteSorter.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index db40c227..21b75350 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -75,6 +75,8 @@ export const RouteSorter: Component = (props) => { {props.currentSort.order === 'asc' ? '↑' : '↓'} )} + {/* Added div for hover effect since gradient effect is using absolute positioning */} +
)} From e6e3361e85f92074cc332f339fdfd2dbbd4a78b1 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Wed, 10 Jul 2024 17:09:10 -0700 Subject: [PATCH 07/20] Remove console log code and changed some stuff --- src/pages/dashboard/components/RouteList.tsx | 42 ++++++-------------- src/utils/sorting.ts | 18 +-------- 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 64b979d9..671975f2 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -17,6 +17,7 @@ const RouteList: Component = (props) => { const [page, setPage] = createSignal(1) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) + const [hasMore, setHasMore] = createSignal(true) const [routesData, { refetch }] = createResource( () => ({ dongleId: props.dongleId, page: page(), pageSize: PAGE_SIZE }), @@ -30,6 +31,7 @@ const RouteList: Component = (props) => { const uniqueNewRoutes = newRoutes.filter(newRoute => !prev.some(existingRoute => existingRoute.start_time === newRoute.start_time), ) + setHasMore(newRoutes.length === PAGE_SIZE) return [...prev, ...uniqueNewRoutes] }) } @@ -38,56 +40,36 @@ const RouteList: Component = (props) => { createEffect(() => { const routes = allRoutes() const currentSortOption = sortOption() - console.log('Current all routes:', routes.map(r => ({ - start_time: r.start_time_utc_millis, - duration: r.duration, - miles: r.length, - engaged: r.engagedDuration, - userFlags: r.userFlags, - }))) - console.log('Current sort option:', currentSortOption) if (routes.length > 0) { void sortAndSetRoutes(routes, currentSortOption) } }) const sortAndSetRoutes = async (routes: RouteSegments[], currentSortOption: SortOption) => { - console.log('Sorting with option:', currentSortOption) const sorted = await sortRoutes(routes, currentSortOption) - console.log('Sorted routes before setting state:', sorted.map(r => ({ - start_time: r.start_time_utc_millis, - duration: r.duration, - miles: r.length, - engaged: r.engagedDuration, - userFlags: r.userFlags, - }))) setSortedRoutes(sorted) - console.log('Routes after setting state:', sortedRoutes().map(r => ({ - start_time: r.start_time_utc_millis, - duration: r.duration, - miles: r.length, - engaged: r.engagedDuration, - userFlags: r.userFlags, - }))) } const handleSortChange = (key: SortKey, order: 'asc' | 'desc') => { const label = key.charAt(0).toUpperCase() + key.slice(1) setSortOption({ label, key, order }) setPage(1) + setAllRoutes([]) + setHasMore(true) void refetch() } - // Add this effect to log sorted routes whenever they change - createEffect(() => { - console.log('Routes at render:', sortedRoutes().map(r => ({ start_time: r.start_time_utc_millis, create_time: r.create_time }))) - }) + const loadMore = () => { + if (!routesData.loading && hasMore()) { + setPage(p => p + 1) + } + } let bottomRef: HTMLDivElement | undefined const observer = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && !routesData.loading) { - setPage(p => p + 1) + if (entries[0].isIntersecting) { + loadMore() } }, { rootMargin: '200px' }, @@ -112,6 +94,8 @@ const RouteList: Component = (props) => {
{routesData.loading &&
Loading...
} + {!routesData.loading && !hasMore() && sortedRoutes().length === 0 &&
No routes found
} + {!routesData.loading && !hasMore() && sortedRoutes().length > 0 &&
All routes loaded
}
) diff --git a/src/utils/sorting.ts b/src/utils/sorting.ts index 6951403d..0ed7a1b8 100644 --- a/src/utils/sorting.ts +++ b/src/utils/sorting.ts @@ -2,7 +2,7 @@ import { RouteSegments } from '~/types' import { getTimelineStatistics, TimelineStatistics } from '~/api/derived' export type SortKey = 'date' | 'miles' | 'duration' | 'engaged' | 'userFlags' -export type SortOrder = 'asc' | 'desc' | null +export type SortOrder = 'asc' | 'desc' | null // ? Do I need null? export interface SortOption { label: string @@ -11,18 +11,14 @@ export interface SortOption { } export const sortRoutes = async (routes: RouteSegments[], option: SortOption): Promise => { - console.log('Sorting routes with option:', option) const { key, order } = option - // Fetch timeline statistics for all routes const statisticsPromises = routes.map(route => getTimelineStatistics(route)) const statistics = await Promise.all(statisticsPromises) - // Create a map of route to its statistics for easy lookup const statsMap = new Map() routes.forEach((route, index) => statsMap.set(route, statistics[index])) - // Add all relevant data to each route object const routesWithData = routes.map(route => ({ ...route, duration: statsMap.get(route)?.duration || 0, @@ -30,7 +26,7 @@ export const sortRoutes = async (routes: RouteSegments[], option: SortOption): P userFlags: statsMap.get(route)?.userFlags || 0, })) - const sortedRoutes = routesWithData.sort((a, b) => { + return routesWithData.sort((a, b) => { let comparison = 0 switch (key) { @@ -53,14 +49,4 @@ export const sortRoutes = async (routes: RouteSegments[], option: SortOption): P return order === 'asc' ? comparison : -comparison }) - - console.log('Sorted routes:', sortedRoutes.map(r => ({ - start_time: r.start_time_utc_millis, - duration: r.duration, - miles: r.length, - engaged: r.engagedDuration, - userFlags: r.userFlags, - }))) - - return sortedRoutes } From 4a5c020e07a7e7b24b5c3bb6e3cbbae69c97c903 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Thu, 11 Jul 2024 18:54:41 -0700 Subject: [PATCH 08/20] TEMP fix for desc to be default - also removed null --- src/components/RouteSorter.tsx | 13 ++++++------- src/pages/dashboard/components/RouteList.tsx | 13 +++++++++++-- src/utils/sorting.ts | 3 +-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index 21b75350..638371c8 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -12,11 +12,11 @@ interface RouteSorterProps { export const RouteSorter: Component = (props) => { const [sortOptions] = createStore([ - { label: 'Date', key: 'date', order: null }, - { label: 'Duration', key: 'duration', order: null }, - { label: 'Miles', key: 'miles', order: null }, - { label: 'Engaged', key: 'engaged', order: null }, - { label: 'User Flags', key: 'userFlags', order: null }, + { label: 'Date', key: 'date', order: 'asc' }, + { label: 'Duration', key: 'duration', order: 'asc' }, + { label: 'Miles', key: 'miles', order: 'asc' }, + { label: 'Engaged', key: 'engaged', order: 'asc' }, + { label: 'User Flags', key: 'userFlags', order: 'asc' }, ]) // Allows mouse wheel to scroll through filters @@ -31,8 +31,7 @@ export const RouteSorter: Component = (props) => { // If the same button is clicked, toggle the order newOrder = props.currentSort.order === 'desc' ? 'asc' : 'desc' } else { - // If a new button is clicked, always start with descending order - newOrder = 'desc' + newOrder = 'asc' // ! Changed to 'asc' so it descends by default... TEMP Solution find out why not working } props.onSortChange(clickedOption.key, newOrder) } diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 671975f2..52a9b270 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -13,7 +13,7 @@ interface RouteListProps { } const RouteList: Component = (props) => { - const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'desc' }) + const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'asc' }) const [page, setPage] = createSignal(1) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) @@ -60,7 +60,9 @@ const RouteList: Component = (props) => { } const loadMore = () => { + console.log('loadMore called', { loading: routesData.loading, hasMore: hasMore() }) if (!routesData.loading && hasMore()) { + console.log('Incrementing page') setPage(p => p + 1) } } @@ -69,6 +71,7 @@ const RouteList: Component = (props) => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { + console.log('Bottom of list visible') loadMore() } }, @@ -84,6 +87,11 @@ const RouteList: Component = (props) => { } }) + createEffect(() => { + page() + void refetch() + }) + onCleanup(() => observer.disconnect()) return ( @@ -92,11 +100,12 @@ const RouteList: Component = (props) => { {(route: RouteSegments) => } -
+
{routesData.loading &&
Loading...
} {!routesData.loading && !hasMore() && sortedRoutes().length === 0 &&
No routes found
} {!routesData.loading && !hasMore() && sortedRoutes().length > 0 &&
All routes loaded
}
+
) } diff --git a/src/utils/sorting.ts b/src/utils/sorting.ts index 0ed7a1b8..647e375f 100644 --- a/src/utils/sorting.ts +++ b/src/utils/sorting.ts @@ -2,8 +2,7 @@ import { RouteSegments } from '~/types' import { getTimelineStatistics, TimelineStatistics } from '~/api/derived' export type SortKey = 'date' | 'miles' | 'duration' | 'engaged' | 'userFlags' -export type SortOrder = 'asc' | 'desc' | null // ? Do I need null? - +export type SortOrder = 'asc' | 'desc' export interface SortOption { label: string key: SortKey From e5e261f0c234cf63d0162afff061d1c12dfb94dc Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Thu, 11 Jul 2024 19:05:00 -0700 Subject: [PATCH 09/20] Added comments - maybe to much --- src/pages/dashboard/components/RouteList.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 52a9b270..f27b1a0b 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -13,30 +13,38 @@ interface RouteListProps { } const RouteList: Component = (props) => { + // Initialize state signals const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'asc' }) const [page, setPage] = createSignal(1) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) const [hasMore, setHasMore] = createSignal(true) + // Create a resource for fetching routes + // ! This might refetch unnecessarily if any of the dependencies change const [routesData, { refetch }] = createResource( () => ({ dongleId: props.dongleId, page: page(), pageSize: PAGE_SIZE }), fetchRoutes, ) + // Effect to update allRoutes when new data is fetched createEffect(() => { const newRoutes = routesData() if (newRoutes) { setAllRoutes(prev => { + // Filter out duplicate routes const uniqueNewRoutes = newRoutes.filter(newRoute => !prev.some(existingRoute => existingRoute.start_time === newRoute.start_time), ) + // Update hasMore based on whether a full page was returned setHasMore(newRoutes.length === PAGE_SIZE) + // Append new unique routes to existing routes return [...prev, ...uniqueNewRoutes] }) } }) + // Effect to sort routes when allRoutes or sortOption changes createEffect(() => { const routes = allRoutes() const currentSortOption = sortOption() @@ -45,20 +53,24 @@ const RouteList: Component = (props) => { } }) + // Function to sort routes and update sortedRoutes signal const sortAndSetRoutes = async (routes: RouteSegments[], currentSortOption: SortOption) => { const sorted = await sortRoutes(routes, currentSortOption) setSortedRoutes(sorted) } + // Handler for sort option changes const handleSortChange = (key: SortKey, order: 'asc' | 'desc') => { const label = key.charAt(0).toUpperCase() + key.slice(1) setSortOption({ label, key, order }) + // Reset pagination and refetch routes setPage(1) setAllRoutes([]) setHasMore(true) void refetch() } + // Function to load more routes const loadMore = () => { console.log('loadMore called', { loading: routesData.loading, hasMore: hasMore() }) if (!routesData.loading && hasMore()) { @@ -67,6 +79,7 @@ const RouteList: Component = (props) => { } } + // Set up Intersection Observer for infinite scrolling let bottomRef: HTMLDivElement | undefined const observer = new IntersectionObserver( (entries) => { @@ -78,6 +91,7 @@ const RouteList: Component = (props) => { { rootMargin: '200px' }, ) + // Effect to observe/unobserve the bottom element createEffect(() => { if (bottomRef) { observer.observe(bottomRef) @@ -87,13 +101,17 @@ const RouteList: Component = (props) => { } }) + // Effect to refetch routes when page changes + // ! This might cause unnecessary refetches if other dependencies of routesData change createEffect(() => { page() void refetch() }) + // Cleanup function to disconnect the observer onCleanup(() => observer.disconnect()) + // Render the component return (
@@ -105,6 +123,7 @@ const RouteList: Component = (props) => { {!routesData.loading && !hasMore() && sortedRoutes().length === 0 &&
No routes found
} {!routesData.loading && !hasMore() && sortedRoutes().length > 0 &&
All routes loaded
}
+ {/* Invisible element for intersection observer */}
) From 25af92fd1b248c6769fde454a7955b5b8f00df20 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Thu, 11 Jul 2024 19:14:28 -0700 Subject: [PATCH 10/20] Added comments and fixed code from previous commit --- src/pages/dashboard/components/RouteList.tsx | 26 +++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index f27b1a0b..ef98d5f5 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -1,4 +1,4 @@ -import { createEffect, createResource, createSignal, For, onCleanup } from 'solid-js' +import { createEffect, createResource, createSignal, For, onCleanup, createMemo } from 'solid-js' import type { Component } from 'solid-js' import { RouteSegments } from '~/types' import { SortOption, SortKey, sortRoutes } from '~/utils/sorting' @@ -6,6 +6,7 @@ import { fetchRoutes } from '~/api/route' import RouteCard from '~/components/RouteCard' import RouteSorter from '~/components/RouteSorter' +// Define the number of routes to fetch per page const PAGE_SIZE = 10 interface RouteListProps { @@ -13,19 +14,22 @@ interface RouteListProps { } const RouteList: Component = (props) => { - // Initialize state signals + // State management using Solid.js signals const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'asc' }) const [page, setPage] = createSignal(1) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) const [hasMore, setHasMore] = createSignal(true) + // Memoize resource parameters to optimize refetching + const resourceParams = createMemo(() => ({ + dongleId: props.dongleId, + page: page(), + pageSize: PAGE_SIZE, + })) + // Create a resource for fetching routes - // ! This might refetch unnecessarily if any of the dependencies change - const [routesData, { refetch }] = createResource( - () => ({ dongleId: props.dongleId, page: page(), pageSize: PAGE_SIZE }), - fetchRoutes, - ) + const [routesData, { refetch }] = createResource(resourceParams, fetchRoutes) // Effect to update allRoutes when new data is fetched createEffect(() => { @@ -76,6 +80,7 @@ const RouteList: Component = (props) => { if (!routesData.loading && hasMore()) { console.log('Incrementing page') setPage(p => p + 1) + void refetch() // Trigger refetch when loading more } } @@ -101,13 +106,6 @@ const RouteList: Component = (props) => { } }) - // Effect to refetch routes when page changes - // ! This might cause unnecessary refetches if other dependencies of routesData change - createEffect(() => { - page() - void refetch() - }) - // Cleanup function to disconnect the observer onCleanup(() => observer.disconnect()) From d836cc60fdc47eb1d1964424333c5798fc6dfe7f Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Fri, 12 Jul 2024 03:19:28 -0700 Subject: [PATCH 11/20] Desc bug was in sorting.ts - desc is now default w/out temp fix --- src/components/RouteSorter.tsx | 12 ++++++------ src/pages/dashboard/components/RouteList.tsx | 2 +- src/utils/sorting.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index 638371c8..20169ba5 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -12,11 +12,11 @@ interface RouteSorterProps { export const RouteSorter: Component = (props) => { const [sortOptions] = createStore([ - { label: 'Date', key: 'date', order: 'asc' }, - { label: 'Duration', key: 'duration', order: 'asc' }, - { label: 'Miles', key: 'miles', order: 'asc' }, - { label: 'Engaged', key: 'engaged', order: 'asc' }, - { label: 'User Flags', key: 'userFlags', order: 'asc' }, + { label: 'Date', key: 'date', order: 'desc' }, + { label: 'Duration', key: 'duration', order: 'desc' }, + { label: 'Miles', key: 'miles', order: 'desc' }, + { label: 'Engaged', key: 'engaged', order: 'desc' }, + { label: 'User Flags', key: 'userFlags', order: 'desc' }, ]) // Allows mouse wheel to scroll through filters @@ -31,7 +31,7 @@ export const RouteSorter: Component = (props) => { // If the same button is clicked, toggle the order newOrder = props.currentSort.order === 'desc' ? 'asc' : 'desc' } else { - newOrder = 'asc' // ! Changed to 'asc' so it descends by default... TEMP Solution find out why not working + newOrder = 'desc' } props.onSortChange(clickedOption.key, newOrder) } diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index ef98d5f5..1a1ac3b2 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -15,7 +15,7 @@ interface RouteListProps { const RouteList: Component = (props) => { // State management using Solid.js signals - const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'asc' }) + const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'desc' }) const [page, setPage] = createSignal(1) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) diff --git a/src/utils/sorting.ts b/src/utils/sorting.ts index 647e375f..1f900185 100644 --- a/src/utils/sorting.ts +++ b/src/utils/sorting.ts @@ -46,6 +46,6 @@ export const sortRoutes = async (routes: RouteSegments[], option: SortOption): P break } - return order === 'asc' ? comparison : -comparison + return order === 'desc' ? comparison : -comparison }) } From 86f6df80e708d498fe1f81965754132185aefef4 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Tue, 16 Jul 2024 09:21:57 -0700 Subject: [PATCH 12/20] Made it full width so it stretches out on desktop --- src/components/RouteSorter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index 20169ba5..d23ae6a6 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -37,7 +37,7 @@ export const RouteSorter: Component = (props) => { } return ( -
+
Date: Tue, 16 Jul 2024 19:28:44 -0700 Subject: [PATCH 13/20] Works again - Rm date btn - infinite scroll works better - performance is ehhh --- src/components/RouteSorter.tsx | 17 +- src/pages/dashboard/components/RouteList.tsx | 167 ++++++++++--------- 2 files changed, 101 insertions(+), 83 deletions(-) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index d23ae6a6..71346adb 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -6,13 +6,12 @@ import { SortKey, SortOption, SortOrder } from '~/utils/sorting' const GRADIENT = 'from-cyan-700 via-blue-800 to-purple-900' interface RouteSorterProps { - onSortChange: (key: SortKey, order: SortOrder) => void + onSortChange: (key: SortKey, order: SortOrder | null) => void currentSort: SortOption } export const RouteSorter: Component = (props) => { const [sortOptions] = createStore([ - { label: 'Date', key: 'date', order: 'desc' }, { label: 'Duration', key: 'duration', order: 'desc' }, { label: 'Miles', key: 'miles', order: 'desc' }, { label: 'Engaged', key: 'engaged', order: 'desc' }, @@ -26,10 +25,16 @@ export const RouteSorter: Component = (props) => { } const handleClick = (clickedOption: SortOption) => { - let newOrder: SortOrder + let newOrder: SortOrder | null if (props.currentSort.key === clickedOption.key) { - // If the same button is clicked, toggle the order - newOrder = props.currentSort.order === 'desc' ? 'asc' : 'desc' + // If the same button is clicked, toggle the order or deactivate the filter + if (props.currentSort.order === 'desc') { + newOrder = 'asc' + } else if (props.currentSort.order === 'asc') { + newOrder = null + } else { + newOrder = 'desc' + } } else { newOrder = 'desc' } @@ -71,7 +76,7 @@ export const RouteSorter: Component = (props) => { {props.currentSort.key === option.key && ( - {props.currentSort.order === 'asc' ? '↑' : '↓'} + {props.currentSort.order === 'asc' ? '↑' : props.currentSort.order === 'desc' ? '↓' : ''} )} {/* Added div for hover effect since gradient effect is using absolute positioning */} diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index cf94cae2..1335ee57 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -1,80 +1,72 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ import { createEffect, - createResource, createSignal, For, Suspense, + onCleanup, } from 'solid-js' import type { VoidComponent } from 'solid-js' import clsx from 'clsx' +import dayjs from 'dayjs' import type { RouteSegments } from '~/types' import RouteCard from '~/components/RouteCard' import { fetcher } from '~/api' -import Button from '~/components/material/Button' import RouteSorter from '~/components/RouteSorter' import { SortOption, SortKey, sortRoutes } from '~/utils/sorting' -const PAGE_SIZE = 3 +const PAGE_SIZE = 7 +const DEFAULT_DAYS = 7 type RouteListProps = { class?: string dongleId: string } -const pages: Promise[] = [] +const fetchRoutesWithinDays = async (dongleId: string, days: number): Promise => { + const now = dayjs().valueOf() + const pastDate = dayjs().subtract(days, 'day').valueOf() + const endpoint = (end: number) => `/v1/devices/${dongleId}/routes_segments?limit=${PAGE_SIZE}&end=${end}` + + let allRoutes: RouteSegments[] = [] + let end = now + + while (true) { + const key = `${endpoint(end)}` + try { + const routes = await fetcher(key) + if (routes.length === 0) break + allRoutes = [...allRoutes, ...routes] + end = routes.at(-1)!.end_time_utc_millis - 1 + if (end < pastDate) break + } catch (error) { + console.error('Error fetching routes:', error) + break + } + } + return allRoutes.filter(route => route.end_time_utc_millis >= pastDate) +} const RouteList: VoidComponent = (props) => { const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'desc' }) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) - const [size, setSize] = createSignal(1) - - 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)!.end_time_utc_millis - return `${endpoint()}&end=${lastSegmentEndTime - 1}` - } - const getPage = (page: number): Promise => { - if (!pages[page]) { - // eslint-disable-next-line no-async-promise-executor - pages[page] = new Promise(async (resolve) => { - const previousPageData = page > 0 ? await getPage(page - 1) : undefined - const key = getKey(previousPageData) - resolve(key ? fetcher(key) : []) - }) - } - return pages[page] - } + const [loading, setLoading] = createSignal(false) + const [days, setDays] = createSignal(DEFAULT_DAYS) createEffect(() => { if (props.dongleId) { - pages.length = 0 - setSize(1) - setAllRoutes([]) - } - }) - - const onLoadMore = () => setSize(size() + 1) - const pageNumbers = () => Array.from(Array(size()).keys()) - - // Effect to update allRoutes when new data is fetched - createEffect(() => { - const fetchData = async () => { - const newRoutes: RouteSegments[] = [] - for (const i of pageNumbers()) { - const routes = await getPage(i) - newRoutes.push(...routes) - } - if (newRoutes.length > 0) { - setAllRoutes(newRoutes) - } + setLoading(true) + fetchRoutesWithinDays(props.dongleId, days()).then(routes => { + setAllRoutes(routes) + setLoading(false) + }).catch(error => { + console.error('Error fetching routes:', error) + setLoading(false) + }) } - void fetchData() }) // Effect to sort routes when allRoutes or sortOption changes @@ -83,22 +75,50 @@ const RouteList: VoidComponent = (props) => { const routes = allRoutes() const currentSortOption = sortOption() if (routes.length > 0) { - const sorted = await sortRoutes(routes, currentSortOption) - setSortedRoutes(sorted) + try { + const sorted = await sortRoutes(routes, currentSortOption) + setSortedRoutes(sorted) + } catch (error) { + console.error('Error sorting routes:', error) + } } } void sortAndSetRoutes() }) // Handler for sort option changes - const handleSortChange = (key: SortKey, order: 'asc' | 'desc') => { - const label = key.charAt(0).toUpperCase() + key.slice(1) - setSortOption({ label, key, order }) - // Reset allRoutes and refetch sorted routes - setAllRoutes([]) - setSize(1) + const handleSortChange = (key: SortKey, order: 'asc' | 'desc' | null) => { + if (order === null) { + setSortOption({ label: 'Date', key: 'date', order: 'desc' }) + } else { + const label = key.charAt(0).toUpperCase() + key.slice(1) + setSortOption({ label, key, order }) + } } + // Infinite scrolling observer + let bottomRef: HTMLDivElement | undefined + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !loading()) { + setLoading(true) + setDays(days => days + DEFAULT_DAYS) + } + }, + { rootMargin: '200px' }, + ) + + createEffect(() => { + if (bottomRef) { + observer.observe(bottomRef) + } + return () => { + if (bottomRef) observer.unobserve(bottomRef) + } + }) + + onCleanup(() => observer.disconnect()) + return (
= (props) => { )} > - - {(i) => { - const [routes] = createResource(() => i, getPage) - return ( - -
-
-
- - } - > - - {(route) => } - - - ) - }} - -
- + +
+
+
+ + } + > + + {(route) => } + + +
+ {loading() &&
Loading more...
}
- {sortedRoutes().length === 0 &&
No routes found
} - {sortedRoutes().length > 0 &&
All routes loaded
} + {sortedRoutes().length === 0 && !loading() &&
No routes found
} + {sortedRoutes().length > 0 && !loading() &&
All routes loaded
}
) From bb5d07e5eb6beafb6d49989dc2159a83e7796c71 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Tue, 16 Jul 2024 19:29:09 -0700 Subject: [PATCH 14/20] Deleted eslint comment, not needed ? --- src/pages/dashboard/components/RouteList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 1335ee57..287ed04b 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-misused-promises */ import { createEffect, createSignal, From f9a0410facf2d7d0ecc3803687fddcbfdb02e231 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Tue, 16 Jul 2024 19:43:02 -0700 Subject: [PATCH 15/20] Skeleton UI shows up faster --- src/pages/dashboard/components/RouteList.tsx | 46 +++++++++++++------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 287ed04b..dd2a9bf7 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ import { createEffect, createSignal, @@ -52,19 +53,24 @@ const RouteList: VoidComponent = (props) => { const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'desc' }) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) - const [loading, setLoading] = createSignal(false) + const [loading, setLoading] = createSignal(true) const [days, setDays] = createSignal(DEFAULT_DAYS) + const loadRoutes = async () => { + setLoading(true) + try { + const routes = await fetchRoutesWithinDays(props.dongleId, days()) + setAllRoutes(routes) + } catch (error) { + console.error('Error fetching routes:', error) + } finally { + setLoading(false) + } + } + createEffect(() => { if (props.dongleId) { - setLoading(true) - fetchRoutesWithinDays(props.dongleId, days()).then(routes => { - setAllRoutes(routes) - setLoading(false) - }).catch(error => { - console.error('Error fetching routes:', error) - setLoading(false) - }) + void loadRoutes() } }) @@ -87,12 +93,14 @@ const RouteList: VoidComponent = (props) => { // Handler for sort option changes const handleSortChange = (key: SortKey, order: 'asc' | 'desc' | null) => { + setLoading(true) if (order === null) { setSortOption({ label: 'Date', key: 'date', order: 'desc' }) } else { const label = key.charAt(0).toUpperCase() + key.slice(1) setSortOption({ label, key, order }) } + void loadRoutes().finally(() => setLoading(false)) } // Infinite scrolling observer @@ -135,16 +143,24 @@ const RouteList: VoidComponent = (props) => { } > - - {(route) => } - + {loading() ? ( + <> +
+
+
+ + ) : ( + + {(route) => } + + )}
- {loading() &&
Loading more...
} + {loading() &&
}
- {sortedRoutes().length === 0 && !loading() &&
No routes found
} - {sortedRoutes().length > 0 && !loading() &&
All routes loaded
} + {!loading() && sortedRoutes().length === 0 &&
No routes found
} + {!loading() && sortedRoutes().length > 0 &&
All routes loaded
}
) From 96724e6d92f40cf40ca9e490998d7a9d1ca0b5a0 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Sat, 20 Jul 2024 17:46:29 -0700 Subject: [PATCH 16/20] Temp UI on card - Faster sorting - Code still needs more work --- src/api/derived.ts | 91 ++++++++-- src/components/RouteCard.tsx | 28 ++- src/components/RouteSorter.tsx | 4 +- src/pages/dashboard/components/RouteList.tsx | 179 +++++++++---------- src/types.d.ts | 4 - src/utils/sorting.ts | 63 +++---- 6 files changed, 213 insertions(+), 156 deletions(-) diff --git a/src/api/derived.ts b/src/api/derived.ts index f2afdb1c..384805bd 100644 --- a/src/api/derived.ts +++ b/src/api/derived.ts @@ -1,5 +1,10 @@ -import type { Route } from '~/types' +import dayjs from 'dayjs' +import type { Route, RouteSegments } from '~/types' import { getRouteDuration } from '~/utils/date' +import { fetcher } from '~/api/index' + +export const PAGE_SIZE = 7 +export const DEFAULT_DAYS = 7 export interface GPSPathPoint { t: number @@ -85,14 +90,33 @@ export interface TimelineStatistics { userFlags: number } -const getDerived = (route: Route, fn: string): Promise => { - let urls: string[] = [] - if (route) { - const segmentNumbers = Array.from({ length: route.maxqlog }, (_, i) => i) - urls = segmentNumbers.map((i) => `${route.url}/${i}/${fn}`) +export interface RouteSegmentsWithStats extends RouteSegments { + timelineStatistics: TimelineStatistics +} + +const fetchWithRetry = async (url: string, attempts: number = 3): Promise => { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + console.log(`Fetching ${url}, attempt ${attempt + 1}`) + const res = await fetch(url) + if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`) + return await res.json() as T + } catch (error) { + console.error(`Attempt ${attempt + 1} failed for ${url}:`, error) + if (attempt === attempts - 1) { + console.error(`Failed to fetch after ${attempts} attempts: ${url}`) + return null + } + } } - const results = urls.map((url) => fetch(url).then((res) => res.json() as T)) - return Promise.all(results) + return null +} + +const getDerived = async (route: Route, fn: string): Promise => { + const segmentNumbers = Array.from({ length: route.maxqlog }, (_, i) => i) + const urls = segmentNumbers.map((i) => `${route.url}/${i}/${fn}`) + const results = await Promise.all(urls.map((url) => fetchWithRetry(url))) + return results.flat().filter(item => item !== null) as T[] } export const getCoords = (route: Route): Promise => @@ -109,12 +133,8 @@ const generateTimelineEvents = ( ): TimelineEvent[] => { const routeDuration = getRouteDuration(route)?.asMilliseconds() ?? 0 - // sort events by timestamp - events.sort((a, b) => { - return a.route_offset_millis - b.route_offset_millis - }) + events.sort((a, b) => a.route_offset_millis - b.route_offset_millis) - // convert events to timeline events const res: TimelineEvent[] = [] let lastEngaged: StateDriveEvent | undefined let lastAlert: StateDriveEvent | undefined @@ -170,7 +190,6 @@ const generateTimelineEvents = ( } }) - // ensure events have an end timestamp if (lastEngaged) { res.push({ type: 'engaged', @@ -227,3 +246,47 @@ export const getTimelineStatistics = async ( getTimelineEvents(route).then((timeline) => generateTimelineStatistics(route, timeline), ) + +export const fetchRoutesWithinDays = async (dongleId: string, days: number): Promise => { + const now = dayjs().valueOf() + const pastDate = dayjs().subtract(days, 'day').valueOf() + const endpoint = (end: number) => `/v1/devices/${dongleId}/routes_segments?limit=${PAGE_SIZE}&end=${end}` + + let allRoutes: RouteSegments[] = [] + let end = now + + while (true) { + const key = `${endpoint(end)}` + try { + const routes = await fetcher(key) + if (!routes || routes.length === 0) break + allRoutes = [...allRoutes, ...routes] + end = (routes.at(-1)?.end_time_utc_millis ?? 0) - 1 + if (end < pastDate) break + } catch (error) { + console.error('Error fetching routes:', error) + break + } + } + return allRoutes.filter(route => route.end_time_utc_millis >= pastDate) +} + +export const fetchRoutesWithStats = async (dongleId: string, days: number): Promise => { + const routes = await fetchRoutesWithinDays(dongleId, days) + console.log('Fetched routes:', routes.length) + const routesWithStats = await Promise.all( + routes.map(async (route): Promise => { + const stats = await getTimelineStatistics(route).catch((error) => { + console.error(`Error fetching statistics for route ${route.fullname}:`, error) + return { duration: 0, engagedDuration: 0, userFlags: 0 } + }) + console.log(`Route ${route.fullname} stats:`, stats) + return { + ...route, + timelineStatistics: stats, + } + }), + ) + console.log('Routes with stats:', routesWithStats.length) + return routesWithStats +} diff --git a/src/components/RouteCard.tsx b/src/components/RouteCard.tsx index 7ee0224f..d34cc9c1 100644 --- a/src/components/RouteCard.tsx +++ b/src/components/RouteCard.tsx @@ -6,8 +6,9 @@ import Card, { CardContent, CardHeader } from '~/components/material/Card' import Icon from '~/components/material/Icon' import RouteStaticMap from '~/components/RouteStaticMap' import RouteStatistics from '~/components/RouteStatistics' - +import { formatRouteDistance, formatRouteDuration } from '~/utils/date' import type { RouteSegments } from '~/types' +import type { SortKey } from '~/utils/sorting' const RouteHeader = (props: { route: RouteSegments }) => { const startTime = () => dayjs(props.route.start_time_utc_millis) @@ -30,10 +31,30 @@ const RouteHeader = (props: { route: RouteSegments }) => { } interface RouteCardProps { - route: RouteSegments + route: RouteSegments & { timelineStatistics?: { duration: number, engagedDuration: number, userFlags: number } } + sortKey: SortKey } const RouteCard: VoidComponent = (props) => { + const getSortedValue = () => { + switch (props.sortKey) { + case 'date': + return dayjs(props.route.start_time_utc_millis).format('YYYY-MM-DD HH:mm:ss') + case 'miles': + return formatRouteDistance(props.route) + case 'duration': + return formatRouteDuration(props.route) + case 'engaged': + return props.route.timelineStatistics ? + `${((props.route.timelineStatistics.engagedDuration / props.route.timelineStatistics.duration) * 100).toFixed(2)}%` : + 'N/A' + case 'userFlags': + return props.route.timelineStatistics?.userFlags.toString() || 'N/A' + default: + return 'N/A' + } + } + return ( @@ -48,6 +69,9 @@ const RouteCard: VoidComponent = (props) => { +
+ {props.sortKey}: {getSortedValue()} +
) diff --git a/src/components/RouteSorter.tsx b/src/components/RouteSorter.tsx index 71346adb..bcb15c90 100644 --- a/src/components/RouteSorter.tsx +++ b/src/components/RouteSorter.tsx @@ -6,8 +6,8 @@ import { SortKey, SortOption, SortOrder } from '~/utils/sorting' const GRADIENT = 'from-cyan-700 via-blue-800 to-purple-900' interface RouteSorterProps { - onSortChange: (key: SortKey, order: SortOrder | null) => void - currentSort: SortOption + onSortChange: (key: SortKey, order: SortOrder | null) => void; + currentSort: SortOption; } export const RouteSorter: Component = (props) => { diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index dd2a9bf7..8c9d1326 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -1,115 +1,109 @@ -/* eslint-disable @typescript-eslint/no-misused-promises */ import { createEffect, createSignal, For, Suspense, + createResource, onCleanup, } from 'solid-js' import type { VoidComponent } from 'solid-js' import clsx from 'clsx' -import dayjs from 'dayjs' import type { RouteSegments } from '~/types' - import RouteCard from '~/components/RouteCard' -import { fetcher } from '~/api' import RouteSorter from '~/components/RouteSorter' -import { SortOption, SortKey, sortRoutes } from '~/utils/sorting' - -const PAGE_SIZE = 7 -const DEFAULT_DAYS = 7 +import { SortOption, SortKey, sortRoutes, SortOrder } from '~/utils/sorting' +import { fetchRoutesWithStats, PAGE_SIZE, DEFAULT_DAYS } from '~/api/derived' + +interface RouteSegmentsWithStats extends RouteSegments { + timelineStatistics: { + duration: number + engagedDuration: number + userFlags: number + } +} type RouteListProps = { class?: string dongleId: string } -const fetchRoutesWithinDays = async (dongleId: string, days: number): Promise => { - const now = dayjs().valueOf() - const pastDate = dayjs().subtract(days, 'day').valueOf() - const endpoint = (end: number) => `/v1/devices/${dongleId}/routes_segments?limit=${PAGE_SIZE}&end=${end}` - - let allRoutes: RouteSegments[] = [] - let end = now - - while (true) { - const key = `${endpoint(end)}` - try { - const routes = await fetcher(key) - if (routes.length === 0) break - allRoutes = [...allRoutes, ...routes] - end = routes.at(-1)!.end_time_utc_millis - 1 - if (end < pastDate) break - } catch (error) { - console.error('Error fetching routes:', error) - break - } - } - return allRoutes.filter(route => route.end_time_utc_millis >= pastDate) +const fetchRoutes = async (dongleId: string, days: number): Promise => { + return await fetchRoutesWithStats(dongleId, days) } const RouteList: VoidComponent = (props) => { const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'desc' }) - const [allRoutes, setAllRoutes] = createSignal([]) - const [sortedRoutes, setSortedRoutes] = createSignal([]) - const [loading, setLoading] = createSignal(true) + const [allRoutes, setAllRoutes] = createSignal([]) + const [sortedRoutes, setSortedRoutes] = createSignal([]) const [days, setDays] = createSignal(DEFAULT_DAYS) + const [hasMore, setHasMore] = createSignal(true) + const [loading, setLoading] = createSignal(true) - const loadRoutes = async () => { - setLoading(true) - try { - const routes = await fetchRoutesWithinDays(props.dongleId, days()) - setAllRoutes(routes) - } catch (error) { - console.error('Error fetching routes:', error) - } finally { - setLoading(false) - } - } + const [routesResource, { refetch }] = createResource( + () => `${props.dongleId}-${days()}`, + () => fetchRoutes(props.dongleId, days()), + ) + + createEffect(() => { + void refetch() + }) createEffect(() => { - if (props.dongleId) { - void loadRoutes() + const routes: RouteSegmentsWithStats[] = routesResource()?.map(route => ({ + ...route, + timelineStatistics: route.timelineStatistics || { duration: 0, engagedDuration: 0, userFlags: 0 }, + })) || [] + + if (routes.length < PAGE_SIZE) { + setHasMore(false) } + + setAllRoutes(routes) + setLoading(false) + console.log('Updated allRoutes:', routes.length) }) - // Effect to sort routes when allRoutes or sortOption changes createEffect(() => { - const sortAndSetRoutes = async () => { - const routes = allRoutes() - const currentSortOption = sortOption() - if (routes.length > 0) { - try { - const sorted = await sortRoutes(routes, currentSortOption) - setSortedRoutes(sorted) - } catch (error) { - console.error('Error sorting routes:', error) - } - } + const routes = allRoutes() + const currentSortOption = sortOption() + console.log('Sorting effect triggered:', { routesCount: routes.length, currentSortOption }) + if (routes.length > 0) { + const sorted = sortRoutes(routes, currentSortOption) + setSortedRoutes(sorted) + console.log('Sorted routes:', sorted.length) + } else { + setSortedRoutes(routes) } - void sortAndSetRoutes() }) - // Handler for sort option changes - const handleSortChange = (key: SortKey, order: 'asc' | 'desc' | null) => { - setLoading(true) - if (order === null) { + const handleSortChange = (key: SortKey) => { + let newOrder: SortOrder | null = 'desc' + const currentSort = sortOption() + + if (currentSort.key === key) { + if (currentSort.order === 'desc') { + newOrder = 'asc' + } else if (currentSort.order === 'asc') { + newOrder = null + } + } + + if (newOrder === null) { + console.log('Reverting to default sort') setSortOption({ label: 'Date', key: 'date', order: 'desc' }) } else { - const label = key.charAt(0).toUpperCase() + key.slice(1) - setSortOption({ label, key, order }) + console.log(`Changing sort to ${key} ${newOrder}`) + setSortOption({ label: key.charAt(0).toUpperCase() + key.slice(1), key, order: newOrder }) } - void loadRoutes().finally(() => setLoading(false)) } - // Infinite scrolling observer let bottomRef: HTMLDivElement | undefined const observer = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && !loading()) { + if (entries[0].isIntersecting && hasMore() && !loading()) { setLoading(true) - setDays(days => days + DEFAULT_DAYS) + setDays((days) => days + DEFAULT_DAYS) } }, { rootMargin: '200px' }, @@ -127,40 +121,29 @@ const RouteList: VoidComponent = (props) => { onCleanup(() => observer.disconnect()) return ( -
+
- -
-
-
- - } - > - {loading() ? ( - <> -
-
-
- - ) : ( - - {(route) => } - - )} + +
+
+
+ + }> + + {(route) => ( + + )} +
- {loading() &&
} + {hasMore() && ( +
+ )}
- {!loading() && sortedRoutes().length === 0 &&
No routes found
} - {!loading() && sortedRoutes().length > 0 &&
All routes loaded
} + {!hasMore() && sortedRoutes().length === 0 &&
No routes found
} + {!hasMore() && sortedRoutes().length > 0 &&
All routes loaded
}
) diff --git a/src/types.d.ts b/src/types.d.ts index ff16e8f5..d4974c3c 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -91,8 +91,4 @@ export interface RouteSegments extends Route { share_exp: RouteShareSignature['exp'] share_sig: RouteShareSignature['sig'] start_time_utc_millis: number - duration?: number - engagedDuration?: number - userFlags?: number - length?: number } diff --git a/src/utils/sorting.ts b/src/utils/sorting.ts index 1f900185..f85475b2 100644 --- a/src/utils/sorting.ts +++ b/src/utils/sorting.ts @@ -1,5 +1,4 @@ -import { RouteSegments } from '~/types' -import { getTimelineStatistics, TimelineStatistics } from '~/api/derived' +import { RouteSegmentsWithStats } from '~/api/derived' export type SortKey = 'date' | 'miles' | 'duration' | 'engaged' | 'userFlags' export type SortOrder = 'asc' | 'desc' @@ -9,43 +8,35 @@ export interface SortOption { order: SortOrder } -export const sortRoutes = async (routes: RouteSegments[], option: SortOption): Promise => { - const { key, order } = option - - const statisticsPromises = routes.map(route => getTimelineStatistics(route)) - const statistics = await Promise.all(statisticsPromises) - - const statsMap = new Map() - routes.forEach((route, index) => statsMap.set(route, statistics[index])) - - const routesWithData = routes.map(route => ({ - ...route, - duration: statsMap.get(route)?.duration || 0, - engagedDuration: statsMap.get(route)?.engagedDuration || 0, - userFlags: statsMap.get(route)?.userFlags || 0, - })) - - return routesWithData.sort((a, b) => { - let comparison = 0 +export const sortRoutes = (routes: RouteSegmentsWithStats[], option: SortOption): RouteSegmentsWithStats[] => { + console.log('Sorting routes with option:', option) + const getSortValue = (route: RouteSegmentsWithStats, key: SortKey): number => { switch (key) { - case 'date': - comparison = b.start_time_utc_millis - a.start_time_utc_millis - break - case 'miles': - comparison = (b.length || 0) - (a.length || 0) - break - case 'duration': - comparison = b.duration - a.duration - break - case 'engaged': - comparison = b.engagedDuration - a.engagedDuration - break - case 'userFlags': - comparison = b.userFlags - a.userFlags - break + case 'date': return route.start_time_utc_millis + case 'miles': return route.length || 0 + case 'duration': return route.timelineStatistics?.duration || 0 + case 'engaged': return route.timelineStatistics?.engagedDuration || 0 + case 'userFlags': return route.timelineStatistics?.userFlags || 0 + default: return 0 } + } - return order === 'desc' ? comparison : -comparison + console.log('First 5 routes before sorting:', routes.slice(0, 5).map(r => ({ + id: r.fullname, + sortValue: getSortValue(r, option.key), + }))) + + const sortedRoutes = [...routes].sort((a, b) => { + const aValue = getSortValue(a, option.key) + const bValue = getSortValue(b, option.key) + return option.order === 'desc' ? bValue - aValue : aValue - bValue }) + + console.log('First 5 routes after sorting:', sortedRoutes.slice(0, 5).map(r => ({ + id: r.fullname, + sortValue: getSortValue(r, option.key), + }))) + + return sortedRoutes } From dd05eee5ccb14600768e1ee8f3430b6ce4e7ba68 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Sat, 20 Jul 2024 18:10:02 -0700 Subject: [PATCH 17/20] Add back skeleton UI --- src/pages/dashboard/components/RouteList.tsx | 47 ++++++++++++-------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 8c9d1326..9a4464f8 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -42,13 +42,14 @@ const RouteList: VoidComponent = (props) => { const [routesResource, { refetch }] = createResource( () => `${props.dongleId}-${days()}`, - () => fetchRoutes(props.dongleId, days()), + async () => { + setLoading(true) + const routes = await fetchRoutes(props.dongleId, days()) + setLoading(false) + return routes + }, ) - createEffect(() => { - void refetch() - }) - createEffect(() => { const routes: RouteSegmentsWithStats[] = routesResource()?.map(route => ({ ...route, @@ -60,7 +61,6 @@ const RouteList: VoidComponent = (props) => { } setAllRoutes(routes) - setLoading(false) console.log('Updated allRoutes:', routes.length) }) @@ -104,6 +104,7 @@ const RouteList: VoidComponent = (props) => { if (entries[0].isIntersecting && hasMore() && !loading()) { setLoading(true) setDays((days) => days + DEFAULT_DAYS) + void refetch() } }, { rootMargin: '200px' }, @@ -123,18 +124,28 @@ const RouteList: VoidComponent = (props) => { return (
- -
-
-
- - }> - - {(route) => ( - - )} - + +
+
+
+ + } + > + {loading() ? ( + <> +
+
+
+ + ) : ( + + {(route) => ( + + )} + + )}
{hasMore() && ( From cd9042f5be40edeecc3e1ea2b6b8f1d62088a2a8 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Sat, 20 Jul 2024 22:22:17 -0700 Subject: [PATCH 18/20] Better sorting logic and performance RouteList --- src/pages/dashboard/components/RouteList.tsx | 26 ++++++-------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 9a4464f8..dc72a3e2 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -38,7 +38,7 @@ const RouteList: VoidComponent = (props) => { const [sortedRoutes, setSortedRoutes] = createSignal([]) const [days, setDays] = createSignal(DEFAULT_DAYS) const [hasMore, setHasMore] = createSignal(true) - const [loading, setLoading] = createSignal(true) + const [loading, setLoading] = createSignal(false) const [routesResource, { refetch }] = createResource( () => `${props.dongleId}-${days()}`, @@ -58,9 +58,11 @@ const RouteList: VoidComponent = (props) => { if (routes.length < PAGE_SIZE) { setHasMore(false) + } else { + setHasMore(true) } - setAllRoutes(routes) + setAllRoutes(prevRoutes => [...prevRoutes, ...routes]) console.log('Updated allRoutes:', routes.length) }) @@ -77,24 +79,13 @@ const RouteList: VoidComponent = (props) => { } }) - const handleSortChange = (key: SortKey) => { - let newOrder: SortOrder | null = 'desc' - const currentSort = sortOption() - - if (currentSort.key === key) { - if (currentSort.order === 'desc') { - newOrder = 'asc' - } else if (currentSort.order === 'asc') { - newOrder = null - } - } - - if (newOrder === null) { + const handleSortChange = (key: SortKey, order: SortOrder | null) => { + if (order === null) { console.log('Reverting to default sort') setSortOption({ label: 'Date', key: 'date', order: 'desc' }) } else { - console.log(`Changing sort to ${key} ${newOrder}`) - setSortOption({ label: key.charAt(0).toUpperCase() + key.slice(1), key, order: newOrder }) + console.log(`Changing sort to ${key} ${order}`) + setSortOption({ label: key.charAt(0).toUpperCase() + key.slice(1), key, order }) } } @@ -102,7 +93,6 @@ const RouteList: VoidComponent = (props) => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore() && !loading()) { - setLoading(true) setDays((days) => days + DEFAULT_DAYS) void refetch() } From 0137ff39080e333a16a32ffec4781e441d9511d3 Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Sat, 20 Jul 2024 22:38:48 -0700 Subject: [PATCH 19/20] Rm duplicate routes - added debouncing data fetches --- src/pages/dashboard/components/RouteList.tsx | 21 ++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index dc72a3e2..1adda054 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -62,8 +62,17 @@ const RouteList: VoidComponent = (props) => { setHasMore(true) } - setAllRoutes(prevRoutes => [...prevRoutes, ...routes]) - console.log('Updated allRoutes:', routes.length) + // Use a Map to filter out duplicate routes based on 'fullname' + const routeMap = new Map() + allRoutes().forEach(route => routeMap.set(route.fullname, route)) + routes.forEach(route => routeMap.set(route.fullname, route)) + + const uniqueRoutes = Array.from(routeMap.values()) + + if (uniqueRoutes.length !== allRoutes().length) { + setAllRoutes(uniqueRoutes) + console.log('Updated allRoutes:', uniqueRoutes.length) + } }) createEffect(() => { @@ -90,11 +99,15 @@ const RouteList: VoidComponent = (props) => { } let bottomRef: HTMLDivElement | undefined + let debounceTimeout: number | undefined const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore() && !loading()) { - setDays((days) => days + DEFAULT_DAYS) - void refetch() + clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => { + setDays((days) => days + DEFAULT_DAYS) + void refetch() + }, 200) as unknown as number } }, { rootMargin: '200px' }, From 2f85f32a934bcffba74a2828dc8c2e6afa79f43c Mon Sep 17 00:00:00 2001 From: David Ratunuman Date: Mon, 22 Jul 2024 22:31:35 -0700 Subject: [PATCH 20/20] Added loading spinnner - Worked on infinite scroll --- src/pages/dashboard/components/RouteList.tsx | 75 +++++++++++--------- tailwind.config.ts | 1 + 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/pages/dashboard/components/RouteList.tsx b/src/pages/dashboard/components/RouteList.tsx index 1adda054..eedba950 100644 --- a/src/pages/dashboard/components/RouteList.tsx +++ b/src/pages/dashboard/components/RouteList.tsx @@ -32,13 +32,26 @@ const fetchRoutes = async (dongleId: string, days: number): Promise) => void>( + func: F, + delay: number, +): ((...args: Parameters) => void) => { + let debounceTimeout: number + return (...args: Parameters) => { + clearTimeout(debounceTimeout) + debounceTimeout = window.setTimeout(() => func(...args), delay) + } +} + const RouteList: VoidComponent = (props) => { const [sortOption, setSortOption] = createSignal({ label: 'Date', key: 'date', order: 'desc' }) const [allRoutes, setAllRoutes] = createSignal([]) const [sortedRoutes, setSortedRoutes] = createSignal([]) const [days, setDays] = createSignal(DEFAULT_DAYS) const [hasMore, setHasMore] = createSignal(true) - const [loading, setLoading] = createSignal(false) + const [loading, setLoading] = createSignal(true) + const [fetchingMore, setFetchingMore] = createSignal(false) + let bottomRef: HTMLDivElement | undefined const [routesResource, { refetch }] = createResource( () => `${props.dongleId}-${days()}`, @@ -56,23 +69,23 @@ const RouteList: VoidComponent = (props) => { timelineStatistics: route.timelineStatistics || { duration: 0, engagedDuration: 0, userFlags: 0 }, })) || [] - if (routes.length < PAGE_SIZE) { - setHasMore(false) - } else { - setHasMore(true) - } + setHasMore(routes.length >= PAGE_SIZE) - // Use a Map to filter out duplicate routes based on 'fullname' const routeMap = new Map() allRoutes().forEach(route => routeMap.set(route.fullname, route)) routes.forEach(route => routeMap.set(route.fullname, route)) const uniqueRoutes = Array.from(routeMap.values()) - - if (uniqueRoutes.length !== allRoutes().length) { - setAllRoutes(uniqueRoutes) - console.log('Updated allRoutes:', uniqueRoutes.length) - } + + setAllRoutes(prevRoutes => { + if (uniqueRoutes.length !== prevRoutes.length) { + console.log('Updated allRoutes:', uniqueRoutes.length) + return uniqueRoutes + } + return prevRoutes + }) + + setFetchingMore(false) }) createEffect(() => { @@ -98,31 +111,24 @@ const RouteList: VoidComponent = (props) => { } } - let bottomRef: HTMLDivElement | undefined - let debounceTimeout: number | undefined - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore() && !loading()) { - clearTimeout(debounceTimeout) - debounceTimeout = setTimeout(() => { + createEffect(() => { + const observer = new IntersectionObserver( + debounce((entries: IntersectionObserverEntry[]) => { + if (entries[0].isIntersecting && hasMore() && !loading() && !fetchingMore()) { + setFetchingMore(true) setDays((days) => days + DEFAULT_DAYS) void refetch() - }, 200) as unknown as number - } - }, - { rootMargin: '200px' }, - ) + } + }, 200), + { rootMargin: '200px' }, + ) - createEffect(() => { if (bottomRef) { observer.observe(bottomRef) } - return () => { - if (bottomRef) observer.unobserve(bottomRef) - } - }) - onCleanup(() => observer.disconnect()) + onCleanup(() => observer.disconnect()) + }) return (
@@ -136,7 +142,7 @@ const RouteList: VoidComponent = (props) => { } > - {loading() ? ( + {loading() && allRoutes().length === 0 ? ( <>
@@ -151,7 +157,12 @@ const RouteList: VoidComponent = (props) => { )}
- {hasMore() && ( + {fetchingMore() && ( +
+
+
+ )} + {hasMore() && !fetchingMore() && (
)}
diff --git a/tailwind.config.ts b/tailwind.config.ts index 29a06ab9..4e9b9eb7 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -201,6 +201,7 @@ export default { indeterminate2: 'indeterminate2 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite', 'circular-rotate': 'circular-rotate 1.4s linear infinite', 'circular-dash': 'circular-dash 1.4s ease-in-out infinite', + 'spin': 'spin 1s linear infinite', }, transitionProperty: { indeterminate: 'transform, background-color',