166 lines
4.6 KiB
TypeScript
166 lines
4.6 KiB
TypeScript
import { SelectInputProps } from "@/components/ui/SelectInput/SelectInput";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
// Валидация даты без зависимости от часовых зон
|
|
// Проверяет что дата существует (например, 31 февраля - невалидна)
|
|
const isValidDate = (year: number, month: number, day: number) => {
|
|
if (!year || !month || !day) return false;
|
|
// Используем Date в локальной зоне только для валидации
|
|
// Это безопасно, т.к. мы не сохраняем результат, только проверяем корректность
|
|
const date = new Date(year, month - 1, day);
|
|
return (
|
|
date.getFullYear() === year &&
|
|
date.getMonth() === month - 1 &&
|
|
date.getDate() === day
|
|
);
|
|
};
|
|
|
|
const parseDateValue = (
|
|
value?: string | null
|
|
): { year: string; month: string; day: string } | null => {
|
|
if (!value) return null;
|
|
|
|
// Поддерживаем форматы: "2003-04-09" и "2003-04-09 12:00"
|
|
const dateMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:\s+\d{2}:\d{2})?$/);
|
|
|
|
if (!dateMatch) return null;
|
|
|
|
const [, year, month, day] = dateMatch;
|
|
return { year, month, day };
|
|
};
|
|
|
|
// Порядок полей даты: месяц-день-год (американский формат)
|
|
// Этот формат используется для всех локалей чтобы обеспечить единообразие
|
|
const getDateInputLocaleFormat = (): ("d" | "m" | "y")[] => {
|
|
// Всегда используем американский формат: месяц, день, год
|
|
return ["m", "d", "y"];
|
|
};
|
|
|
|
interface UseDateInputProps {
|
|
value?: string | null;
|
|
onChange?: (value: string | null) => void;
|
|
maxYear?: number;
|
|
yearsRange?: number;
|
|
}
|
|
|
|
export const useDateInput = ({
|
|
value,
|
|
onChange,
|
|
maxYear = new Date().getFullYear() - 11,
|
|
yearsRange = 100,
|
|
}: UseDateInputProps) => {
|
|
const [year, setYear] = useState("");
|
|
const [month, setMonth] = useState("");
|
|
const [day, setDay] = useState("");
|
|
|
|
const lastEmittedValue = useRef<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const parsedDate = parseDateValue(value);
|
|
|
|
if (parsedDate) {
|
|
setYear(parsedDate.year);
|
|
setMonth(parsedDate.month);
|
|
setDay(parsedDate.day);
|
|
} else {
|
|
setYear("");
|
|
setMonth("");
|
|
setDay("");
|
|
}
|
|
}, [value]);
|
|
|
|
const updateValue = useCallback(
|
|
(newValue: string | null) => {
|
|
if (newValue !== lastEmittedValue.current) {
|
|
lastEmittedValue.current = newValue;
|
|
onChange?.(newValue);
|
|
}
|
|
},
|
|
[onChange]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const numericYear = Number(year);
|
|
const numericMonth = Number(month);
|
|
const numericDay = Number(day);
|
|
|
|
if (isValidDate(numericYear, numericMonth, numericDay)) {
|
|
const formattedDate = `${year}-${month}-${day}`;
|
|
updateValue(formattedDate);
|
|
} else {
|
|
if (year || month || day) {
|
|
updateValue(null);
|
|
}
|
|
}
|
|
}, [year, month, day, updateValue]);
|
|
|
|
const yearOptions = useMemo(
|
|
() =>
|
|
Array.from({ length: yearsRange }, (_, i) => ({
|
|
value: maxYear - i,
|
|
label: String(maxYear - i),
|
|
})),
|
|
[maxYear, yearsRange]
|
|
);
|
|
|
|
const monthOptions = useMemo(() => {
|
|
const monthNames = [
|
|
"January", "February", "March", "April", "May", "June",
|
|
"July", "August", "September", "October", "November", "December"
|
|
];
|
|
|
|
return Array.from({ length: 12 }, (_, i) => ({
|
|
value: String(i + 1).padStart(2, "0"),
|
|
label: monthNames[i],
|
|
}));
|
|
}, []);
|
|
|
|
const dayOptions = useMemo(() => {
|
|
const daysInMonth =
|
|
year && month ? new Date(Number(year), Number(month), 0).getDate() : 31;
|
|
return Array.from({ length: daysInMonth }, (_, i) => ({
|
|
value: String(i + 1).padStart(2, "0"),
|
|
label: String(i + 1),
|
|
}));
|
|
}, [year, month]);
|
|
|
|
const localeFormat = useMemo(
|
|
() => getDateInputLocaleFormat(),
|
|
[]
|
|
);
|
|
|
|
const handleYearChange: SelectInputProps["onValueChange"] = useCallback(
|
|
(value: string) => {
|
|
setYear(value);
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleMonthChange: SelectInputProps["onValueChange"] = useCallback(
|
|
(value: string) => {
|
|
setMonth(value);
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleDayChange: SelectInputProps["onValueChange"] = useCallback(
|
|
(value: string) => {
|
|
setDay(value);
|
|
},
|
|
[]
|
|
);
|
|
|
|
return {
|
|
year,
|
|
month,
|
|
day,
|
|
yearOptions,
|
|
monthOptions,
|
|
dayOptions,
|
|
localeFormat,
|
|
handleYearChange,
|
|
handleMonthChange,
|
|
handleDayChange,
|
|
};
|
|
};
|