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