Skip to content

Commit

Permalink
feat: poll stop preds while popup open. (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
morganney committed Sep 14, 2023
1 parent bb771d6 commit bf01c46
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 46 deletions.
94 changes: 85 additions & 9 deletions packages/ui/src/components/stop.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<StopProps> = ({ stop, route, direction, arrivals, isLoading = false }) => {
const Stop: FC<StopProps> = ({ 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 ? (
<em key={epochTime}>Arriving</em>
) : (
<time key={epochTime} dateTime={`PT${minutes}M`}>
{minutes} min
</time>
)
})

useEffect(() => {
if (popup) {
popup.update()
}
}, [popup])

if (error) {
const msg = error instanceof Error ? error.message : 'An unexpected error occured.'

return <p>Unable to retrieve arrival times for this stop. {msg}</p>
}

return (
<Definition>
<dt>Route</dt>
Expand All @@ -48,10 +124,10 @@ const Stop: FC<StopProps> = ({ stop, route, direction, arrivals, isLoading = fal
<dd>{stop?.title}</dd>
<dt>Arrivals</dt>
<dd>
{isLoading ? (
{isFetching ? (
<Skeleton display="block" />
) : arrivals?.length ? (
arrivals?.join(', ')
arrivals
) : (
'No arrivals'
)}
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/globals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { BusmapGlobals } from './types'

const defaultGlobals = {
dispatch: () => {},
agency: undefined,
route: null,
center: { lat: 32.79578, lon: -95.45166 },
bounds: {
Expand Down
7 changes: 6 additions & 1 deletion packages/ui/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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(<BusMap />)
root.render(
<StrictMode>
<BusMap />
</StrictMode>
)
73 changes: 39 additions & 34 deletions packages/ui/src/layout.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -22,12 +24,10 @@ const getDirectionForStop = (id: string, directions: Direction[]) => {

return directions[0].title
}

export interface LayoutProps {
children: ReactNode
}
export const Layout: FC<LayoutProps> = ({ children }) => {
const { bounds, route, agency, dispatch } = useContext(Globals)
const Layout: FC<LayoutProps> = ({ 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
Expand All @@ -52,8 +52,11 @@ export const Layout: FC<LayoutProps> = ({ 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]))
})
Expand All @@ -62,30 +65,9 @@ export const Layout: FC<LayoutProps> = ({ 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(
<Stop isLoading stop={stop} route={route} direction={direction} />
)
)
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(
<Stop stop={stop} route={route} direction={direction} arrivals={arrivals} />
)
)
marker.addTo(map).bindPopup(popup.current)
marker.on('click', () => {
dispatch({ type: 'selected', value: { stop, direction } })
})
})
}
Expand All @@ -95,5 +77,28 @@ export const Layout: FC<LayoutProps> = ({ children }) => {
}
}, [bounds, agency, route, dispatch])

if (selected && agency && route) {
const { stop, direction } = selected

return (
<>
{children}
{createPortal(
<Stop
agency={agency}
stop={stop}
route={route}
direction={direction}
popup={popup.current}
/>,
content.current
)}
</>
)
}

return children
}

export { Layout }
export type { LayoutProps }
5 changes: 4 additions & 1 deletion packages/ui/src/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
10 changes: 10 additions & 0 deletions packages/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ interface Prediction {
messages: string[]
values: Pred[]
}
interface Selection {
stop: Stop
direction: string
}

// Busmap types
interface BoundsChanged {
Expand All @@ -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<BusmapAction>
center: Point
bounds: Bounds
agency?: string
stop?: Stop
selected?: { stop: Stop; direction: string }
arrivals?: string[]
route: Route | null
}
Expand Down

0 comments on commit bf01c46

Please sign in to comment.