horizontal-calendar: switch months by arrow buttons

This commit is contained in:
vchikalkin 2025-06-08 17:21:31 +03:00
parent 0cb9e6b6ee
commit d085a3d24d

View File

@ -2,115 +2,61 @@
import { cn } from '../../lib/utils';
import { Button } from '../ui/button';
import { addDays, format, isSameDay, subDays } from 'date-fns';
import {
addMonths,
eachDayOfInterval,
endOfMonth,
format,
isSameDay,
startOfMonth,
} from 'date-fns';
import { ru } from 'date-fns/locale';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
type HorizontalCalendarProps = {
readonly className?: string;
readonly numberOfDays?: number;
readonly numberOfDaysBefore?: number;
readonly onDateChange: (date: Date) => void;
readonly selectedDate: Date;
};
export function HorizontalCalendar({
className,
numberOfDays = 14,
numberOfDaysBefore = 7,
onDateChange,
selectedDate,
}: HorizontalCalendarProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const [baseDate, setBaseDate] = useState(addDays(new Date(), -numberOfDaysBefore));
const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
const [currentMonthDate, setCurrentMonthDate] = useState(startOfMonth(new Date()));
// Получаем список всех дней текущего месяца
const dates = useMemo(() => {
return Array.from({ length: numberOfDays }, (_, index) => {
return addDays(baseDate, index);
return eachDayOfInterval({
end: endOfMonth(currentMonthDate),
start: startOfMonth(currentMonthDate),
});
}, [baseDate, numberOfDays]);
}, [currentMonthDate]);
const updateCurrentMonth = (newBaseDate: Date) => {
setBaseDate(newBaseDate);
setCurrentMonthDate(newBaseDate);
setTimeout(() => {
if (scrollRef.current) {
scrollRef.current.scrollLeft = 0;
}
}, 0);
const scrollToStart = () => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
behavior: 'smooth',
left: 0,
});
}
};
const scrollPrevious = () => {
const newDate = subDays(baseDate, numberOfDays);
updateCurrentMonth(newDate);
const newDate = addMonths(currentMonthDate, -1);
setCurrentMonthDate(newDate);
setTimeout(scrollToStart, 0);
};
const scrollNext = () => {
const newDate = addDays(baseDate, numberOfDays);
updateCurrentMonth(newDate);
const newDate = addMonths(currentMonthDate, 1);
setCurrentMonthDate(newDate);
setTimeout(scrollToStart, 0);
};
// Обработчик scroll — вычисляет дату ближайшую к левому краю
const handleScroll = () => {
const container = scrollRef.current;
if (!container) return;
const children = Array.from(container.children) as HTMLElement[];
let closestChild: HTMLElement | null = null;
let minOffset = Infinity;
for (const child of children) {
const offset = Math.abs(child.offsetLeft - container.scrollLeft);
if (offset < minOffset) {
minOffset = offset;
closestChild = child;
}
}
if (closestChild) {
const dateString = closestChild.dataset.date;
if (dateString) {
const date = new Date(dateString);
setCurrentMonthDate(date);
}
}
};
useEffect(() => {
const scrollElement = scrollRef.current;
if (scrollElement) {
scrollElement.addEventListener('scroll', handleScroll, { passive: true });
}
return () => {
if (scrollElement) {
scrollElement.removeEventListener('scroll', handleScroll);
}
};
}, []);
useEffect(() => {
const container = scrollRef.current;
if (!container) return;
const selectedChild = container.querySelector(
`[data-date="${selectedDate.toISOString()}"]`,
) as HTMLElement;
if (selectedChild) {
// Прокрутка так, чтобы элемент оказался в центре
const offsetLeft = selectedChild.offsetLeft;
const centerOffset = offsetLeft - container.clientWidth / 2 + selectedChild.clientWidth / 2;
container.scrollTo({
behavior: 'auto', // можно 'smooth', если хочешь плавно
left: centerOffset,
});
}
}, []); // <- только при монтировании
return (
<div className={cn('w-full', className)}>
<div className="mb-2 flex items-center justify-between">
@ -131,6 +77,7 @@ export function HorizontalCalendar({
</Button>
</div>
</div>
<div className="relative">
<div className="scrollbar-hide flex snap-x overflow-x-auto pb-2" ref={scrollRef}>
{dates.map((date) => (