import React, { useEffect, useMemo, useRef, useState } from 'react'; import * as TimeService from '@/os/TimeService'; import { colors } from '../res/colors'; import { strings } from '../res/strings'; import { stringsEn } from '../res/strings.en'; import { useAppStrings } from '../../../os/useAppStrings'; import { buildReminderTimestamp, clampReminderSelection, getReminderDateOptions, getReminderHourOptions, getReminderMinuteOptions, startOfLocalDay, } from './dateTimePickerModel'; type PickerOption = { value: T; label: string; }; const ITEM_HEIGHT = 56; const PAD_HEIGHT = ITEM_HEIGHT; const VISIBLE_HEIGHT = ITEM_HEIGHT * 3; const SELECTED_TEXT = '#4A86FF'; const MUTED_TEXT = '#C6C6C6'; function pad2(n: number) { return n.toString().padStart(2, '0'); } const PickerColumn = ({ value, options, onChange, className = '', }: { value: T; options: PickerOption[]; onChange: (value: T) => void; className?: string; }) => { const containerRef = useRef(null); const scrollTimerRef = useRef(null); const selectedIndex = useMemo( () => options.findIndex(option => option.value === value), [options, value], ); useEffect(() => { if (!containerRef.current || selectedIndex < 0) return; containerRef.current.scrollTop = selectedIndex * ITEM_HEIGHT; }, [selectedIndex, options]); useEffect(() => { return () => { if (scrollTimerRef.current) { window.clearTimeout(scrollTimerRef.current); } }; }, []); const snapToNearest = () => { const el = containerRef.current; if (!el) return; const index = Math.max(0, Math.min(Math.round(el.scrollTop / ITEM_HEIGHT), options.length - 1)); el.scrollTo({ top: index * ITEM_HEIGHT, behavior: 'smooth' }); }; const scheduleSnap = () => { if (scrollTimerRef.current) { window.clearTimeout(scrollTimerRef.current); } scrollTimerRef.current = window.setTimeout(() => { snapToNearest(); }, 80); }; const handleScroll = () => { const el = containerRef.current; if (!el) return; const index = Math.max(0, Math.min(Math.round(el.scrollTop / ITEM_HEIGHT), options.length - 1)); const next = options[index]; if (next && next.value !== value) { onChange(next.value); } scheduleSnap(); }; return (
{options.map(option => { const selected = option.value === value; return ( ); })}
); }; export const DateTimeDialog: React.FC<{ title: string; initialTimestamp?: number; onCancel: () => void; onConfirm: (timestamp: number) => void; onClear?: () => void; }> = ({ initialTimestamp, onCancel, onConfirm, onClear }) => { const s = useAppStrings(strings, stringsEn); const [openedAtTs] = useState(() => TimeService.now()); const [selectedTs, setSelectedTs] = useState(() => clampReminderSelection(initialTimestamp ?? openedAtTs, openedAtTs), ); const [reminderEnabled, setReminderEnabled] = useState(() => !onClear || typeof initialTimestamp === 'number'); const selectedDate = TimeService.fromTimestamp(selectedTs); const selectedDayStart = startOfLocalDay(selectedTs); const selectedHour = selectedDate.getHours(); const selectedMinute = selectedDate.getMinutes(); const dateOptions = useMemo(() => getReminderDateOptions(openedAtTs), [openedAtTs]); const hourOptions = useMemo( () => getReminderHourOptions(selectedDayStart, openedAtTs), [openedAtTs, selectedDayStart], ); const minuteOptions = useMemo( () => getReminderMinuteOptions(selectedDayStart, selectedHour, openedAtTs), [openedAtTs, selectedDayStart, selectedHour], ); useEffect(() => { if (!hourOptions.includes(selectedHour)) { const fallbackHour = hourOptions[0]; setSelectedTs(prev => { const prevDate = TimeService.fromTimestamp(prev); return clampReminderSelection( buildReminderTimestamp(selectedDayStart, fallbackHour, prevDate.getMinutes()), openedAtTs, ); }); return; } if (!minuteOptions.includes(selectedMinute)) { const fallbackMinute = minuteOptions[0]; setSelectedTs(prev => { const prevDate = TimeService.fromTimestamp(prev); return clampReminderSelection( buildReminderTimestamp(selectedDayStart, prevDate.getHours(), fallbackMinute), openedAtTs, ); }); } }, [hourOptions, minuteOptions, openedAtTs, selectedDayStart, selectedHour, selectedMinute]); return (
) : null}
{ setSelectedTs(prev => { const prevDate = TimeService.fromTimestamp(prev); return clampReminderSelection( buildReminderTimestamp(dayStart, prevDate.getHours(), prevDate.getMinutes()), openedAtTs, ); }); }} className="flex-[1.8]" /> ({ value: hour, label: `${pad2(hour)} ${s.datetime_hour_suffix}` }))} onChange={(hour) => { setSelectedTs(prev => { const prevDate = TimeService.fromTimestamp(prev); return clampReminderSelection( buildReminderTimestamp(selectedDayStart, hour, prevDate.getMinutes()), openedAtTs, ); }); }} className="flex-1" /> ({ value: minute, label: `${pad2(minute)} ${s.datetime_minute_suffix}` }))} onChange={(minute) => { setSelectedTs( clampReminderSelection( buildReminderTimestamp(selectedDayStart, selectedHour, minute), openedAtTs, ), ); }} className="flex-1" />
); }; export default DateTimeDialog;