Skip to content

Commit 47733c1

Browse files
authored
Merge pull request #7297 from StoDevX/drew/bus-schedule-detail
Bus schedule detail view and day of week picker
2 parents 043fa05 + 7a2e7c9 commit 47733c1

File tree

9 files changed

+563
-36
lines changed

9 files changed

+563
-36
lines changed

modules/context-menu/index.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import {StyleProp, ViewStyle} from 'react-native'
33
import {Touchable} from '@frogpond/touchable'
4-
import {ContextMenuButton} from 'react-native-ios-context-menu'
4+
import {ContextMenuButton, MenuState} from 'react-native-ios-context-menu'
55
import {upperFirst} from 'lodash'
66

77
interface ContextMenuProps {
@@ -12,6 +12,7 @@ interface ContextMenuProps {
1212
isMenuPrimaryAction?: boolean
1313
onPress?: () => void
1414
onPressMenuItem: (menuKey: string) => void | Promise<void>
15+
selectedAction?: string
1516
title: string
1617
}
1718

@@ -27,20 +28,25 @@ export const ContextMenu = React.forwardRef<
2728
isMenuPrimaryAction,
2829
onPress,
2930
onPressMenuItem,
31+
selectedAction,
3032
title,
3133
} = props
3234

3335
let menuItems = React.useMemo(() => {
34-
return actions.map((option) => ({
35-
actionKey: option,
36-
actionTitle: upperFirst(option),
37-
}))
38-
}, [actions])
36+
return actions.map((option) => {
37+
const menuState: MenuState = selectedAction === option ? 'on' : 'off'
38+
return {
39+
actionKey: option,
40+
actionTitle: upperFirst(option),
41+
menuState,
42+
}
43+
})
44+
}, [actions, selectedAction])
3945

4046
return (
4147
<ContextMenuButton
4248
ref={ref}
43-
enableContextMenu={!disabled ?? false}
49+
enableContextMenu={!disabled}
4450
isMenuPrimaryAction={isMenuPrimaryAction ?? false}
4551
menuConfig={{
4652
menuTitle: title ?? '',

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: 9 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,11 @@ export type MiscViewParamList = {
8386
CourseDetail: {course: CourseType}
8487
StudentOrgsDetail: {org: StudentOrgType}
8588
BusMapView: {line: UnprocessedBusLine}
89+
BusRouteDetail: {
90+
stop: BusTimetableEntry
91+
line: UnprocessedBusLine
92+
subtitle: string
93+
}
8694
MenuItemDetail: {item: MenuItem; icons: MasterCorIconMapType}
8795
PrinterList: {job: PrintJob}
8896
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}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import * as React from 'react'
2+
import {
3+
ColorValue,
4+
ScrollView,
5+
StyleSheet,
6+
Text,
7+
TouchableOpacity,
8+
View,
9+
} from 'react-native'
10+
import type {Moment} from 'moment-timezone'
11+
import type {DayOfWeek} from '../types'
12+
import * as c from '@frogpond/colors'
13+
import {ContextMenu} from '@frogpond/context-menu'
14+
import {Icon} from '@frogpond/icon'
15+
16+
const styles = StyleSheet.create({
17+
dayPickerContainer: {
18+
backgroundColor: c.systemGroupedBackground,
19+
paddingVertical: 12,
20+
paddingHorizontal: 16,
21+
},
22+
dayPickerScroll: {
23+
flexGrow: 0,
24+
},
25+
dayButton: {
26+
paddingHorizontal: 12,
27+
paddingVertical: 8,
28+
marginRight: 8,
29+
borderRadius: 6,
30+
backgroundColor: c.secondarySystemGroupedBackground,
31+
},
32+
dayButtonSelected: {
33+
backgroundColor: c.systemBlue,
34+
},
35+
dayButtonText: {
36+
fontSize: 14,
37+
fontWeight: '500',
38+
color: c.label,
39+
},
40+
dayButtonTextSelected: {
41+
color: c.white,
42+
},
43+
resetButton: {
44+
marginTop: 8,
45+
alignSelf: 'center',
46+
paddingHorizontal: 12,
47+
paddingVertical: 6,
48+
},
49+
resetButtonText: {
50+
fontSize: 14,
51+
color: c.systemRed,
52+
fontWeight: '500',
53+
},
54+
headerButtonContainer: {
55+
borderWidth: 1,
56+
borderRadius: 6,
57+
paddingHorizontal: 8,
58+
paddingVertical: 6,
59+
backgroundColor: c.systemBackground,
60+
alignItems: 'center',
61+
justifyContent: 'center',
62+
flexDirection: 'row',
63+
gap: 6,
64+
},
65+
headerButtonText: {
66+
fontSize: 14,
67+
fontWeight: '500',
68+
},
69+
})
70+
71+
export const DAYS_OF_WEEK: Array<{day: DayOfWeek; label: string}> = [
72+
{day: 'Su', label: 'Sunday'},
73+
{day: 'Mo', label: 'Monday'},
74+
{day: 'Tu', label: 'Tuesday'},
75+
{day: 'We', label: 'Wednesday'},
76+
{day: 'Th', label: 'Thursday'},
77+
{day: 'Fr', label: 'Friday'},
78+
{day: 'Sa', label: 'Saturday'},
79+
]
80+
81+
export type DayPickerProps = {
82+
selectedDay: DayOfWeek
83+
onDaySelect: (day: DayOfWeek) => void
84+
currentDay: DayOfWeek
85+
onReset?: () => void
86+
}
87+
88+
export const DayPicker = ({
89+
selectedDay,
90+
onDaySelect,
91+
currentDay,
92+
onReset,
93+
}: DayPickerProps): JSX.Element => {
94+
const isOverridden = selectedDay !== currentDay
95+
96+
return (
97+
<View style={styles.dayPickerContainer}>
98+
<ScrollView
99+
contentContainerStyle={styles.dayPickerScroll}
100+
horizontal={true}
101+
showsHorizontalScrollIndicator={false}
102+
>
103+
{DAYS_OF_WEEK.map(({day, label}) => {
104+
const isSelected = selectedDay === day
105+
return (
106+
<TouchableOpacity
107+
key={day}
108+
onPress={() => onDaySelect(day)}
109+
style={[styles.dayButton, isSelected && styles.dayButtonSelected]}
110+
>
111+
<Text
112+
style={[
113+
styles.dayButtonText,
114+
isSelected && styles.dayButtonTextSelected,
115+
]}
116+
>
117+
{label}
118+
</Text>
119+
</TouchableOpacity>
120+
)
121+
})}
122+
</ScrollView>
123+
{isOverridden && onReset && (
124+
<TouchableOpacity onPress={onReset} style={styles.resetButton}>
125+
<Text style={styles.resetButtonText}>Reset</Text>
126+
</TouchableOpacity>
127+
)}
128+
</View>
129+
)
130+
}
131+
132+
export const momentToDayOfWeek = (moment: Moment): DayOfWeek => {
133+
const dayMap: Record<number, DayOfWeek> = {
134+
0: 'Su',
135+
1: 'Mo',
136+
2: 'Tu',
137+
3: 'We',
138+
4: 'Th',
139+
5: 'Fr',
140+
6: 'Sa',
141+
}
142+
return dayMap[moment.day()]
143+
}
144+
145+
export const createMomentForDay = (
146+
baseMoment: Moment,
147+
targetDay: DayOfWeek,
148+
): Moment => {
149+
const dayMap: Record<DayOfWeek, number> = {
150+
Su: 0,
151+
Mo: 1,
152+
Tu: 2,
153+
We: 3,
154+
Th: 4,
155+
Fr: 5,
156+
Sa: 6,
157+
}
158+
159+
const targetDayNumber = dayMap[targetDay]
160+
const currentDayNumber = baseMoment.day()
161+
const diff = targetDayNumber - currentDayNumber
162+
163+
return baseMoment.clone().add(diff, 'days')
164+
}
165+
166+
export type DayPickerHeaderProps = {
167+
selectedDay: DayOfWeek
168+
onDaySelect: (day: DayOfWeek) => void
169+
currentDay: DayOfWeek
170+
accentColor?: ColorValue
171+
}
172+
173+
export const DayPickerHeader = ({
174+
selectedDay,
175+
onDaySelect,
176+
currentDay,
177+
accentColor = c.systemBlue,
178+
}: DayPickerHeaderProps): JSX.Element => {
179+
const dayOptions = DAYS_OF_WEEK.map(({label}) => label)
180+
const selectedIndex = DAYS_OF_WEEK.findIndex(({day}) => day === selectedDay)
181+
const selectedLabel = DAYS_OF_WEEK[selectedIndex]?.label || 'Today'
182+
183+
const isOverridden = selectedDay !== currentDay
184+
const displayText = isOverridden ? selectedLabel : 'Today'
185+
186+
return (
187+
<ContextMenu
188+
actions={dayOptions}
189+
isMenuPrimaryAction={true}
190+
onPressMenuItem={(item: string) => {
191+
const selectedDayData = DAYS_OF_WEEK.find(({label}) => label === item)
192+
if (selectedDayData) {
193+
onDaySelect(selectedDayData.day)
194+
}
195+
}}
196+
selectedAction={selectedLabel}
197+
title="Pick a schedule"
198+
>
199+
<View style={[styles.headerButtonContainer, {borderColor: accentColor}]}>
200+
<Icon color={accentColor} name="calendar" size={16} />
201+
<Text style={[styles.headerButtonText, {color: accentColor}]}>
202+
{displayText}
203+
</Text>
204+
</View>
205+
</ContextMenu>
206+
)
207+
}

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')

0 commit comments

Comments
 (0)