w-funnel/src/hooks/useDateInput.ts
2025-10-09 20:29:40 +02:00

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,
};
};