Skip to content

Commit d23aacb

Browse files
committed
add detail view for bus schedules
1 parent 043fa05 commit d23aacb

File tree

7 files changed

+248
-18
lines changed

7 files changed

+248
-18
lines changed

source/navigation/routes.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import * as settings from '../views/settings/'
2626
import * as streaming from '../views/streaming'
2727
import * as orgs from '../views/student-orgs'
2828
import * as transportation from '../views/transportation'
29+
import {BusRouteDetail} from '../views/transportation/bus/detail'
2930
import * as stoprint from '../views/stoprint'
3031
import * as more from '../views/more'
3132
import * as directory from '../views/directory'
@@ -120,6 +121,13 @@ const HomeStackScreens = () => {
120121
name={transportation.NavigationKey}
121122
options={transportation.NavigationOptions}
122123
/>
124+
<Stack.Screen
125+
component={BusRouteDetail}
126+
name="BusRouteDetail"
127+
options={({route}) => ({
128+
title: `${route.params.line.line} Schedule`,
129+
})}
130+
/>
123131
</Stack.Group>
124132
<Stack.Group>
125133
<Stack.Screen

source/navigation/types.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import {ContactType} from '../views/contacts/types'
1818
import {StudentOrgType} from '../views/student-orgs/types'
1919
import {RouteParams as HoursEditorType} from '../views/building-hours/report/editor'
2020
import {WordType} from '../views/dictionary/types'
21-
import {UnprocessedBusLine} from '../views/transportation/bus/types'
21+
import {
22+
UnprocessedBusLine,
23+
BusTimetableEntry,
24+
} from '../views/transportation/bus/types'
2225
import type {
2326
MasterCorIconMapType,
2427
MenuItemType as MenuItem,
@@ -83,6 +86,7 @@ export type MiscViewParamList = {
8386
CourseDetail: {course: CourseType}
8487
StudentOrgsDetail: {org: StudentOrgType}
8588
BusMapView: {line: UnprocessedBusLine}
89+
BusRouteDetail: {stop: BusTimetableEntry; line: UnprocessedBusLine}
8690
MenuItemDetail: {item: MenuItem; icons: MasterCorIconMapType}
8791
PrinterList: {job: PrintJob}
8892
PrintJobRelease: {job: PrintJob; printer?: Printer}

source/views/transportation/bus/components/bus-stop-row.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@ export function BusStopRow(props: Props): JSX.Element {
6565
]
6666

6767
return (
68-
<ListRow fullHeight={true} fullWidth={true} style={styles.row}>
68+
<ListRow
69+
arrowPosition="center"
70+
fullHeight={true}
71+
fullWidth={true}
72+
style={styles.row}
73+
>
6974
<ProgressChunk
7075
barColor={barColor}
7176
currentStopColor={currentStopColor}

source/views/transportation/bus/components/times.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const TIME_FORMAT = 'h:mma'
77

88
type Props = {
99
times: DepartureTimeList
10-
style: StyleProp<TextStyle>
10+
style?: StyleProp<TextStyle>
1111
}
1212

1313
export function ScheduleTimes({times, style}: Props): JSX.Element {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const BUS_FOOTER_HEADLINE =
2+
'Bus routes and times subject to change without notice'
3+
const BUS_FOOTER_SUBHEADLINE = 'Data collected by the humans of All About Olaf'
4+
5+
export const BUS_FOOTER_MESSAGE = [
6+
BUS_FOOTER_HEADLINE,
7+
BUS_FOOTER_SUBHEADLINE,
8+
].join('\n\n')
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import * as React from 'react'
2+
import {useEffect, useState} from 'react'
3+
import {FlatList, StyleSheet, Text} from 'react-native'
4+
5+
import {RouteProp, useRoute} from '@react-navigation/native'
6+
import type {Moment} from 'moment-timezone'
7+
8+
import {ScheduleTimes} from './components/times'
9+
import {ProgressChunk} from './components/progress-chunk'
10+
import type {BusTimetableEntry, UnprocessedBusLine, BusSchedule} from './types'
11+
import {RootStackParamList} from '../../../navigation/types'
12+
import {
13+
BusStateEnum,
14+
getCurrentBusIteration,
15+
getScheduleForNow,
16+
processBusLine,
17+
findBusStopStatus as findStopStatus,
18+
type BusStopStatusEnum,
19+
} from './lib'
20+
21+
import {ListFooter, ListRow, ListSectionHeader} from '@frogpond/lists'
22+
import * as c from '@frogpond/colors'
23+
import {useMomentTimer} from '@frogpond/timer'
24+
import {timezone} from '@frogpond/constants'
25+
import {Column} from '@frogpond/layout'
26+
import {Detail, Title} from '@frogpond/lists'
27+
import {BUS_FOOTER_MESSAGE} from './constants'
28+
29+
const styles = StyleSheet.create({
30+
container: {
31+
backgroundColor: c.secondarySystemGroupedBackground,
32+
},
33+
timeRow: {
34+
flexDirection: 'row',
35+
},
36+
noTimesText: {
37+
color: c.tertiaryLabel,
38+
fontStyle: 'italic',
39+
textAlign: 'center',
40+
padding: 20,
41+
},
42+
internalPadding: {
43+
paddingVertical: 12,
44+
},
45+
skippingStopTitle: {
46+
color: c.tertiaryLabel,
47+
},
48+
passedStopTitle: {
49+
color: c.secondaryLabel,
50+
},
51+
atStopTitle: {
52+
fontWeight: '600',
53+
},
54+
})
55+
56+
type Props = {
57+
stop: BusTimetableEntry
58+
line: UnprocessedBusLine
59+
now: Moment
60+
}
61+
62+
function BusStopDetailInternal(props: Props): JSX.Element {
63+
let {stop, line, now} = props
64+
65+
let [_, setSchedule] = useState<BusSchedule | null>(null)
66+
let [currentBusIteration, setCurrentBusIteration] = useState<number | null>(
67+
null,
68+
)
69+
let [status, setStatus] = useState<BusStateEnum>('none')
70+
71+
useEffect(() => {
72+
let processedLine = processBusLine(line, now)
73+
let scheduleForToday = getScheduleForNow(processedLine.schedules, now)
74+
let {index, status: currentStatus} = getCurrentBusIteration(
75+
scheduleForToday,
76+
now,
77+
)
78+
79+
setSchedule(scheduleForToday)
80+
setStatus(currentStatus)
81+
setCurrentBusIteration(index)
82+
}, [line, now])
83+
84+
let departureTimes = stop.departures.filter(Boolean)
85+
let stopStatus = findStopStatus({
86+
stop,
87+
busStatus: status,
88+
departureIndex: currentBusIteration,
89+
now,
90+
})
91+
92+
let headerElement = (
93+
<ListSectionHeader subtitle="Departures" title={stop.name} />
94+
)
95+
96+
let rowTextStyle = [
97+
stopStatus === 'skip' && styles.skippingStopTitle,
98+
stopStatus === 'after' && styles.passedStopTitle,
99+
stopStatus === 'at' && styles.atStopTitle,
100+
]
101+
102+
if (departureTimes.length === 0) {
103+
let emptyRowElement = (
104+
<ListRow fullHeight={true} fullWidth={true} style={styles.timeRow}>
105+
<ProgressChunk
106+
barColor={line.colors.bar}
107+
currentStopColor={line.colors.dot}
108+
isFirstChunk={true}
109+
isLastChunk={true}
110+
stopStatus={stopStatus}
111+
/>
112+
<Column flex={1} style={styles.internalPadding}>
113+
<Title bold={false} style={rowTextStyle}>
114+
{stop.name}
115+
</Title>
116+
<Detail lines={1}>
117+
<Text style={styles.noTimesText}>No departure times available</Text>
118+
</Detail>
119+
</Column>
120+
</ListRow>
121+
)
122+
123+
return (
124+
<FlatList
125+
ListFooterComponent={<ListFooter title={BUS_FOOTER_MESSAGE} />}
126+
ListHeaderComponent={headerElement}
127+
data={[emptyRowElement]}
128+
keyExtractor={(item, index) => `${item.key}-${index}`}
129+
renderItem={({item}) => item}
130+
style={styles.container}
131+
/>
132+
)
133+
}
134+
135+
const getTimeStatus = (departureTime: Moment | null): BusStopStatusEnum => {
136+
if (!departureTime) return 'skip'
137+
138+
if (now.isAfter(departureTime, 'minute')) {
139+
return 'after'
140+
} else if (now.isSame(departureTime, 'minute')) {
141+
return 'at'
142+
} else {
143+
return 'before'
144+
}
145+
}
146+
147+
let timeRows = departureTimes.map((time, index) => {
148+
let timeStatus = getTimeStatus(time)
149+
150+
let timeRowTextStyle = [
151+
timeStatus === 'skip' && styles.skippingStopTitle,
152+
timeStatus === 'after' && styles.passedStopTitle,
153+
timeStatus === 'at' && styles.atStopTitle,
154+
]
155+
156+
return (
157+
<ListRow
158+
key={index}
159+
fullHeight={true}
160+
fullWidth={true}
161+
style={styles.timeRow}
162+
>
163+
<ProgressChunk
164+
barColor={line.colors.bar}
165+
currentStopColor={line.colors.dot}
166+
isFirstChunk={index === 0}
167+
isLastChunk={index === departureTimes.length - 1}
168+
stopStatus={timeStatus}
169+
/>
170+
<Column flex={1} style={styles.internalPadding}>
171+
<Title bold={false} style={timeRowTextStyle}>
172+
<ScheduleTimes times={[time]} />
173+
</Title>
174+
</Column>
175+
</ListRow>
176+
)
177+
})
178+
179+
return (
180+
<FlatList
181+
ItemSeparatorComponent={undefined}
182+
ListFooterComponent={<ListFooter title={BUS_FOOTER_MESSAGE} />}
183+
ListHeaderComponent={headerElement}
184+
data={timeRows}
185+
keyExtractor={(item, index) => `${item.key}-${index}`}
186+
renderItem={({item}) => item}
187+
style={styles.container}
188+
/>
189+
)
190+
}
191+
192+
export function BusRouteDetail(): JSX.Element {
193+
let {now} = useMomentTimer({intervalMs: 1000 * 60, timezone: timezone()})
194+
let route = useRoute<RouteProp<RootStackParamList, 'BusRouteDetail'>>()
195+
let {stop, line} = route.params
196+
197+
return <BusStopDetailInternal line={line} now={now} stop={stop} />
198+
}

source/views/transportation/bus/line.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22
import {useEffect, useState} from 'react'
3-
import {FlatList, Platform, StyleSheet, Text} from 'react-native'
3+
import {FlatList, Platform, StyleSheet, Text, TouchableOpacity} from 'react-native'
44
import type {BusSchedule, UnprocessedBusLine} from './types'
55
import {
66
BusStateEnum,
@@ -16,6 +16,8 @@ import {BusStopRow} from './components/bus-stop-row'
1616
import {ListFooter, ListRow, ListSectionHeader} from '@frogpond/lists'
1717
import {InfoHeader} from '@frogpond/info-header'
1818
import * as c from '@frogpond/colors'
19+
import {useNavigation} from '@react-navigation/native'
20+
import {BUS_FOOTER_MESSAGE} from './constants'
1921

2022
const styles = StyleSheet.create({
2123
container: {
@@ -117,6 +119,7 @@ function deriveFromProps({line, now}: {line: UnprocessedBusLine; now: Moment}) {
117119

118120
export function BusLine(props: Props): JSX.Element {
119121
let {line, now} = props
122+
let navigation = useNavigation()
120123

121124
let [schedule, setSchedule] = useState<BusSchedule | null>(null)
122125
let [subtitle, setSubtitle] = useState<string>()
@@ -150,10 +153,8 @@ export function BusLine(props: Props): JSX.Element {
150153
)
151154

152155
let lineMessage = line.notice || ''
153-
let footerMessage =
154-
'Bus routes and times subject to change without notice\n\nData collected by the humans of All About Olaf'
155156

156-
let footerElement = <ListFooter title={footerMessage} />
157+
let footerElement = <ListFooter title={BUS_FOOTER_MESSAGE} />
157158
let headerElement = lineMessage ? (
158159
<>
159160
<InfoHeader message={lineMessage} title={`About ${line.line}`} />
@@ -172,18 +173,24 @@ export function BusLine(props: Props): JSX.Element {
172173
ListFooterComponent={footerElement}
173174
ListHeaderComponent={headerElement}
174175
data={timetable}
175-
keyExtractor={(item, index) => index.toString()}
176+
keyExtractor={(item, index) => `${item.name}-${index}`}
176177
renderItem={({item, index}) => (
177-
<BusStopRow
178-
barColor={line.colors.bar}
179-
currentStopColor={line.colors.dot}
180-
departureIndex={currentBusIteration}
181-
isFirstRow={index === 0}
182-
isLastRow={timetable.length === 0 || index === timetable.length - 1}
183-
now={now}
184-
status={status}
185-
stop={item}
186-
/>
178+
<TouchableOpacity
179+
onPress={() => {
180+
navigation.navigate('BusRouteDetail', {stop: item, line})
181+
}}
182+
>
183+
<BusStopRow
184+
barColor={line.colors.bar}
185+
currentStopColor={line.colors.dot}
186+
departureIndex={currentBusIteration}
187+
isFirstRow={index === 0}
188+
isLastRow={timetable.length === 0 || index === timetable.length - 1}
189+
now={now}
190+
status={status}
191+
stop={item}
192+
/>
193+
</TouchableOpacity>
187194
)}
188195
style={styles.container}
189196
/>

0 commit comments

Comments
 (0)