import type { FC } from '../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useMemo, useState, } from '../../lib/teact/teact'; import type { LangFn } from '../../hooks/useLang'; import { MAX_INT_32 } from '../../config'; import buildClassName from '../../util/buildClassName'; import { formatDateToString, formatTime, getDayStart } from '../../util/date/dateFormat'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import usePrevious from '../../hooks/usePrevious'; import Button from '../ui/Button'; import Modal from '../ui/Modal'; import './CalendarModal.scss'; const MAX_SAFE_DATE = MAX_INT_32 * 1000; const MIN_SAFE_DATE = 0; export type OwnProps = { selectedAt?: number; minAt?: number; maxAt?: number; isFutureMode?: boolean; isPastMode?: boolean; isOpen: boolean; withTimePicker?: boolean; submitButtonLabel?: string; secondButtonLabel?: string; onClose: () => void; onSubmit: (date: Date) => void; onSecondButtonClick?: NoneToVoidFunction; }; const WEEKDAY_LETTERS = [ 'lng_weekday1', 'lng_weekday2', 'lng_weekday3', 'lng_weekday4', 'lng_weekday5', 'lng_weekday6', 'lng_weekday7', ]; const CalendarModal: FC = ({ selectedAt, minAt, maxAt, isFutureMode, isPastMode, isOpen, withTimePicker, submitButtonLabel, secondButtonLabel, onClose, onSubmit, onSecondButtonClick, }) => { const lang = useLang(); const now = new Date(); const minDate = useMemo(() => { if (isFutureMode && !minAt) return new Date(); return new Date(Math.max(minAt || MIN_SAFE_DATE, MIN_SAFE_DATE)); }, [isFutureMode, minAt]); const maxDate = useMemo(() => { if (isPastMode && !maxAt) return new Date(); return new Date(Math.min(maxAt || MAX_SAFE_DATE, MAX_SAFE_DATE)); }, [isPastMode, maxAt]); const passedSelectedDate = useMemo(() => (selectedAt ? new Date(selectedAt) : new Date()), [selectedAt]); const prevIsOpen = usePrevious(isOpen); const [isTimeInputFocused, markTimeInputAsFocused, unmarkTimeInputAsFocused] = useFlag(false); const [selectedDate, setSelectedDate] = useState(passedSelectedDate); const [currentMonthAndYear, setCurrentMonthAndYear] = useState( new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1), ); const [selectedHours, setSelectedHours] = useState( formatInputTime(passedSelectedDate.getHours()), ); const [selectedMinutes, setSelectedMinutes] = useState( formatInputTime(passedSelectedDate.getMinutes()), ); const selectedDay = formatDay(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); const currentYear = currentMonthAndYear.getFullYear(); const currentMonth = currentMonthAndYear.getMonth(); useEffect(() => { if (!prevIsOpen && isOpen) { setSelectedDate(passedSelectedDate); setCurrentMonthAndYear(new Date(passedSelectedDate.getFullYear(), passedSelectedDate.getMonth(), 1)); if (withTimePicker) { setSelectedHours(formatInputTime(passedSelectedDate.getHours())); setSelectedMinutes(formatInputTime(passedSelectedDate.getMinutes())); } } }, [passedSelectedDate, isOpen, prevIsOpen, withTimePicker]); useEffect(() => { if (isFutureMode && !isTimeInputFocused && selectedDate.getTime() < minDate.getTime()) { setSelectedDate(minDate); setSelectedHours(formatInputTime(minDate.getHours())); setSelectedMinutes(formatInputTime(minDate.getMinutes())); } }, [isFutureMode, isTimeInputFocused, minDate, selectedDate]); useEffect(() => { if (isPastMode && !isTimeInputFocused && selectedDate.getTime() > maxDate.getTime()) { setSelectedDate(maxDate); setSelectedHours(formatInputTime(maxDate.getHours())); setSelectedMinutes(formatInputTime(maxDate.getMinutes())); } }, [isFutureMode, isPastMode, isTimeInputFocused, maxDate, minDate, selectedDate]); useEffect(() => { if (selectedAt) { const newSelectedDate = new Date(selectedAt); setSelectedDate(newSelectedDate); setSelectedHours(formatInputTime(newSelectedDate.getHours())); setSelectedMinutes(formatInputTime(newSelectedDate.getMinutes())); } }, [selectedAt]); const shouldDisableNextMonth = (isPastMode && currentYear >= now.getFullYear() && currentMonth >= now.getMonth()) || (maxDate && currentYear >= maxDate.getFullYear() && currentMonth >= maxDate.getMonth()); const shouldDisablePrevMonth = isFutureMode && currentYear <= now.getFullYear() && currentMonth <= now.getMonth(); const { prevMonthGrid, currentMonthGrid, nextMonthGrid } = useMemo(() => ( buildCalendarGrid(currentYear, currentMonth) ), [currentMonth, currentYear]); const submitLabel = useMemo(() => { return submitButtonLabel || formatSubmitLabel(lang, selectedDate); }, [lang, selectedDate, submitButtonLabel]); function handlePrevMonth() { setCurrentMonthAndYear((d) => { const dateCopy = new Date(d); dateCopy.setMonth(dateCopy.getMonth() - 1); return dateCopy; }); } function handleNextMonth() { setCurrentMonthAndYear((d) => { const dateCopy = new Date(d); dateCopy.setMonth(dateCopy.getMonth() + 1); return dateCopy; }); } function handleDateSelect(date: number) { setSelectedDate((d) => { const dateCopy = new Date(d); dateCopy.setDate(date); dateCopy.setMonth(currentMonth); dateCopy.setFullYear(currentYear); return dateCopy; }); } const handleSubmit = useCallback(() => { onSubmit(selectedDate); }, [onSubmit, selectedDate]); const handleChangeHours = useCallback((e: React.ChangeEvent) => { const value = e.target.value.replace(/[^\d]+/g, ''); if (!value.length) { setSelectedHours(''); e.target.value = ''; return; } const hours = Math.max(0, Math.min(Number(value), 23)); const date = new Date(selectedDate.getTime()); date.setHours(hours); setSelectedDate(date); const hoursStr = formatInputTime(hours); setSelectedHours(hoursStr); e.target.value = hoursStr; }, [selectedDate]); const handleChangeMinutes = useCallback((e: React.ChangeEvent) => { const value = e.target.value.replace(/[^\d]+/g, ''); if (!value.length) { setSelectedMinutes(''); e.target.value = ''; return; } const minutes = Math.max(0, Math.min(Number(value), 59)); const date = new Date(selectedDate.getTime()); date.setMinutes(minutes); setSelectedDate(date); const minutesStr = formatInputTime(minutes); setSelectedMinutes(minutesStr); e.target.value = minutesStr; }, [selectedDate]); function renderTimePicker() { return (
:
); } return (

{lang(`lng_month${currentMonth + 1}`)} {' '} {currentYear}

{WEEKDAY_LETTERS.map((day) => (
{lang(day)}
))} {prevMonthGrid.map((gridDate) => (
{gridDate}
))} {currentMonthGrid.map((gridDate) => (
handleDateSelect(gridDate)} className={buildClassName( 'day-button', 'div-button', isDisabledDay( currentYear, currentMonth, gridDate, minDate, maxDate, ) ? 'disabled' : `${gridDate ? 'clickable' : ''}`, selectedDay === formatDay(currentYear, currentMonth, gridDate) && 'selected', )} > {Boolean(gridDate) && ( {gridDate} )}
))} {nextMonthGrid.map((gridDate) => (
{gridDate}
))}
{withTimePicker && renderTimePicker()}
{secondButtonLabel && ( )}
); }; function buildCalendarGrid(year: number, month: number) { const prevMonthGrid: number[] = []; const currentMonthGrid: number[] = []; const nextMonthGrid: number[] = []; const date = new Date(); date.setDate(1); date.setMonth(month); date.setFullYear(year); const firstDay = date.getDay() || 7; const totalDaysInPrevMonth = new Date(year, month, 0).getDate(); for (let i = 1; i < firstDay; i++) { prevMonthGrid.push(totalDaysInPrevMonth - firstDay + i + 1); } while (date.getMonth() === month) { const gridDate = date.getDate(); currentMonthGrid.push(gridDate); date.setDate(gridDate + 1); } const lastRowDaysCount = (currentMonthGrid.length + prevMonthGrid.length) % 7; if (lastRowDaysCount > 0) { for (let i = 1; i <= 7 - lastRowDaysCount; i++) { nextMonthGrid.push(i); } } return { prevMonthGrid, currentMonthGrid, nextMonthGrid }; } function isDisabledDay(year: number, month: number, day: number, minDate?: Date, maxDate?: Date) { const selectedDay = new Date(year, month, day); const fixedMinDate = minDate && getDayStart(minDate); const fixedMaxDate = maxDate && getDayStart(maxDate); if (fixedMaxDate && selectedDay > fixedMaxDate) { return true; } else if (fixedMinDate && selectedDay < fixedMinDate) { return true; } return false; } function formatInputTime(value: string | number) { return String(value).padStart(2, '0'); } function formatDay(year: number, month: number, day: number) { return `${year}-${month + 1}-${day}`; } function formatSubmitLabel(lang: LangFn, date: Date) { const day = formatDateToString(date, lang.code); const today = formatDateToString(new Date(), lang.code); if (day === today) { return lang('Conversation.ScheduleMessage.SendToday', formatTime(lang, date)); } return lang('Conversation.ScheduleMessage.SendOn', [day, formatTime(lang, date)]); } export default memo(CalendarModal);