From bf01c4684c801628be330d4e3f31ea8845e9b846 Mon Sep 17 00:00:00 2001 From: Morgan Ney Date: Thu, 14 Sep 2023 16:11:46 -0500 Subject: [PATCH] feat: poll stop preds while popup open. (#8) --- packages/ui/src/components/stop.tsx | 94 ++++++++++++++++++++++++++--- packages/ui/src/globals.tsx | 1 - packages/ui/src/index.tsx | 7 ++- packages/ui/src/layout.tsx | 73 +++++++++++----------- packages/ui/src/providers.tsx | 5 +- packages/ui/src/types.ts | 10 +++ 6 files changed, 144 insertions(+), 46 deletions(-) diff --git a/packages/ui/src/components/stop.tsx b/packages/ui/src/components/stop.tsx index d2d9c9d..729bd94 100644 --- a/packages/ui/src/components/stop.tsx +++ b/packages/ui/src/components/stop.tsx @@ -1,17 +1,39 @@ -import styled from 'styled-components' +import styled, { keyframes } from 'styled-components' +import { useEffect } from 'react' +import { useQuery } from 'react-query' import { Skeleton } from '@busmap/components/skeleton' +import { getForStop } from '../api/rb/predictions.js' + import type { FC } from 'react' +import type { Popup } from 'leaflet' import type { Stop, Route } from '../types.js' interface StopProps { - stop?: Stop - route?: Route - direction?: string - arrivals?: string[] - isLoading?: boolean + stop: Stop + agency: string + route: Route + direction: string + popup: Popup } +const blink = keyframes` + 10% { + opacity: 0; + } + 20% { + opacity: 0; + } + 30% { + opacity: 0; + } + 40% { + opacity: 0.5; + } + 50% { + opacity: 1; + } +` const Definition = styled.dl` display: grid; grid-template-rows: repeat(3, max-content); @@ -36,8 +58,62 @@ const Definition = styled.dl` dt::after { content: ':'; } + + dd:last-child { + display: flex; + gap: 5px; + } + dd:last-child { + em { + font-style: normal; + opacity: 1; + animation: ${blink} 1.5s linear infinite; + } + em:not(:last-child)::after, + time:not(:last-child)::after { + content: ','; + } + } ` -const Stop: FC = ({ stop, route, direction, arrivals, isLoading = false }) => { +const Stop: FC = ({ stop, agency, route, direction, popup }) => { + const { data, error, isFetching } = useQuery( + ['preds', agency, route.id, stop.id], + () => getForStop(agency, route.id, stop.id), + { + refetchOnWindowFocus: true, + refetchInterval: 8_000 + } + ) + const arrivals = !data?.length + ? [] + : /** + * Given that the agency and route are defined, + * the first prediction's values should suffice, + * i.e. no other predictions for different routes, + * or agencies for the selected stop. + */ + data[0].values.slice(0, 3).map(({ minutes, epochTime }) => { + return minutes === 0 ? ( + Arriving + ) : ( + + ) + }) + + useEffect(() => { + if (popup) { + popup.update() + } + }, [popup]) + + if (error) { + const msg = error instanceof Error ? error.message : 'An unexpected error occured.' + + return

Unable to retrieve arrival times for this stop. {msg}

+ } + return (
Route
@@ -48,10 +124,10 @@ const Stop: FC = ({ stop, route, direction, arrivals, isLoading = fal
{stop?.title}
Arrivals
- {isLoading ? ( + {isFetching ? ( ) : arrivals?.length ? ( - arrivals?.join(', ') + arrivals ) : ( 'No arrivals' )} diff --git a/packages/ui/src/globals.tsx b/packages/ui/src/globals.tsx index 83bd220..d94025f 100644 --- a/packages/ui/src/globals.tsx +++ b/packages/ui/src/globals.tsx @@ -4,7 +4,6 @@ import type { BusmapGlobals } from './types' const defaultGlobals = { dispatch: () => {}, - agency: undefined, route: null, center: { lat: 32.79578, lon: -95.45166 }, bounds: { diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 8483af5..6e3197c 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -1,7 +1,12 @@ import { createRoot } from 'react-dom/client' +import { StrictMode } from 'react' import { BusMap } from './app.js' const root = createRoot(document.querySelector('main') as HTMLElement) -root.render() +root.render( + + + +) diff --git a/packages/ui/src/layout.tsx b/packages/ui/src/layout.tsx index 14e9c9a..5a6f07d 100644 --- a/packages/ui/src/layout.tsx +++ b/packages/ui/src/layout.tsx @@ -1,15 +1,17 @@ import L from 'leaflet' -import { useEffect, useContext } from 'react' -import { renderToStaticMarkup } from 'react-dom/server' +import { useEffect, useContext, useRef } from 'react' +import { createPortal } from 'react-dom' import { Globals } from './globals.js' -import { getForStop } from './api/rb/predictions.js' import { Stop } from './components/stop.js' import type { FC, ReactNode } from 'react' import type { Direction } from './types.js' type Point = [number, number] +interface LayoutProps { + children: ReactNode +} const getDirectionForStop = (id: string, directions: Direction[]) => { for (const direction of directions) { @@ -22,12 +24,10 @@ const getDirectionForStop = (id: string, directions: Direction[]) => { return directions[0].title } - -export interface LayoutProps { - children: ReactNode -} -export const Layout: FC = ({ children }) => { - const { bounds, route, agency, dispatch } = useContext(Globals) +const Layout: FC = ({ children }) => { + const { bounds, route, agency, selected, dispatch } = useContext(Globals) + const content = useRef(document.createElement('div')) + const popup = useRef(L.popup()) useEffect(() => { const main = document.querySelector('main') as HTMLElement @@ -52,8 +52,11 @@ export const Layout: FC = ({ children }) => { iconUrl: '../assets/svg/circled.svg', iconSize: [7, 7] }) - const popup = L.popup() + popup.current.setContent(content.current) + popup.current.on('remove', () => { + dispatch({ type: 'selected', value: undefined }) + }) route.paths.forEach(path => { polylines.push(path.points.map(({ lat, lon }) => [lat, lon])) }) @@ -62,30 +65,9 @@ export const Layout: FC = ({ children }) => { const direction = getDirectionForStop(stop.id, route.directions) const marker = L.marker([stop.lat, stop.lon], { icon }) - marker.addTo(map).bindPopup(popup) - marker.on('click', async () => { - marker.setPopupContent( - renderToStaticMarkup( - - ) - ) - const preds = await getForStop(agency, route.id, stop.id) - const arrivals = !preds.length - ? [] - : /** - * Given that the agency, route, and stop - * are defined the first prediction's values - * should suffice. - */ - preds[0].values - .slice(0, 3) - .map(({ minutes }) => `${minutes === 0 ? 'Arriving' : `${minutes} min`}`) - - marker.setPopupContent( - renderToStaticMarkup( - - ) - ) + marker.addTo(map).bindPopup(popup.current) + marker.on('click', () => { + dispatch({ type: 'selected', value: { stop, direction } }) }) }) } @@ -95,5 +77,28 @@ export const Layout: FC = ({ children }) => { } }, [bounds, agency, route, dispatch]) + if (selected && agency && route) { + const { stop, direction } = selected + + return ( + <> + {children} + {createPortal( + , + content.current + )} + + ) + } + return children } + +export { Layout } +export type { LayoutProps } diff --git a/packages/ui/src/providers.tsx b/packages/ui/src/providers.tsx index 59c5b3f..fdcbd4f 100644 --- a/packages/ui/src/providers.tsx +++ b/packages/ui/src/providers.tsx @@ -14,9 +14,12 @@ const reducer = (state: BusmapState, action: BusmapAction): BusmapState => { case 'agency': return { ...state, agency: action.value } case 'route': - return { ...state, route: action.value } + return { ...state, selected: undefined, route: action.value } case 'stop': return { ...state, stop: action.value } + case 'selected': { + return { ...state, selected: action.value } + } default: return { ...defaults, ...state } } diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index b313d85..de615d7 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -73,6 +73,10 @@ interface Prediction { messages: string[] values: Pred[] } +interface Selection { + stop: Stop + direction: string +} // Busmap types interface BoundsChanged { @@ -95,18 +99,24 @@ interface StopChanged { type: 'stop' value: Stop } +interface SelectedChanged { + type: 'selected' + value: Selection | undefined +} type BusmapAction = | BoundsChanged | CenterChanged | AgencyChanged | RouteChanged | StopChanged + | SelectedChanged interface BusmapGlobals { dispatch: Dispatch center: Point bounds: Bounds agency?: string stop?: Stop + selected?: { stop: Stop; direction: string } arrivals?: string[] route: Route | null }