|
| 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 | +} |
0 commit comments