uchill/front_material/components/common/DatePicker.tsx

300 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Material Design 3 Date Picker
*/
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, addMonths, subMonths, startOfWeek, endOfWeek } from 'date-fns';
import { ru } from 'date-fns/locale';
interface DatePickerProps {
value: string; // YYYY-MM-DD format
onChange: (value: string) => void;
disabled?: boolean;
required?: boolean;
}
export const DatePicker: React.FC<DatePickerProps> = ({
value,
onChange,
disabled = false,
required = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [displayMonth, setDisplayMonth] = useState(value ? new Date(value) : new Date());
const containerRef = useRef<HTMLDivElement>(null);
const selectedDate = value ? new Date(value) : null;
// Закрываем picker при клике вне компонента
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const handleDateSelect = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
onChange(`${year}-${month}-${day}`);
setIsOpen(false);
};
// Получаем дни для отображения в календаре
const getDaysInMonth = () => {
const start = startOfMonth(displayMonth);
const end = endOfMonth(displayMonth);
const startDate = startOfWeek(start, { locale: ru });
const endDate = endOfWeek(end, { locale: ru });
return eachDayOfInterval({ start: startDate, end: endDate });
};
const days = getDaysInMonth();
const weekDays = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
return (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
{/* Input field */}
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
style={{
width: '100%',
padding: '12px 16px',
fontSize: '16px',
color: value ? 'var(--md-sys-color-on-surface)' : 'var(--md-sys-color-on-surface-variant)',
background: 'var(--md-sys-color-surface)',
border: `1px solid ${isOpen ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'}`,
borderWidth: isOpen ? '2px' : '1px',
borderRadius: '4px',
fontFamily: 'inherit',
cursor: disabled ? 'not-allowed' : 'pointer',
outline: 'none',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '12px',
textAlign: 'left',
}}
>
<span>
{selectedDate ? format(selectedDate, 'd MMMM yyyy', { locale: ru }) : 'Выберите дату'}
</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
</button>
{/* Calendar dropdown */}
{isOpen && (
<div style={{
position: 'absolute',
top: 'calc(100% + 4px)',
left: 0,
background: 'var(--md-sys-color-surface)',
border: '1px solid var(--md-sys-color-outline-variant)',
borderRadius: '16px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
zIndex: 1000,
padding: '16px',
minWidth: '320px',
}}>
{/* Header with month/year navigation */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}>
<button
type="button"
onClick={() => setDisplayMonth(subMonths(displayMonth, 1))}
style={{
width: '32px',
height: '32px',
padding: 0,
background: 'transparent',
border: 'none',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface)',
transition: 'background 0.2s ease',
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<div style={{
fontSize: '16px',
fontWeight: '500',
color: 'var(--md-sys-color-on-surface)',
}}>
{format(displayMonth, 'LLLL yyyy', { locale: ru })}
</div>
<button
type="button"
onClick={() => setDisplayMonth(addMonths(displayMonth, 1))}
style={{
width: '32px',
height: '32px',
padding: 0,
background: 'transparent',
border: 'none',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface)',
transition: 'background 0.2s ease',
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
{/* Week days header */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: '4px',
marginBottom: '8px',
}}>
{weekDays.map(day => (
<div
key={day}
style={{
textAlign: 'center',
fontSize: '12px',
fontWeight: '500',
color: 'var(--md-sys-color-on-surface-variant)',
padding: '8px 0',
}}
>
{day}
</div>
))}
</div>
{/* Calendar days grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: '4px',
}}>
{days.map((day, index) => {
const isSelected = selectedDate && isSameDay(day, selectedDate);
const isCurrentMonth = isSameMonth(day, displayMonth);
const isToday = isSameDay(day, new Date());
return (
<button
key={index}
type="button"
onClick={() => handleDateSelect(day)}
style={{
width: '40px',
height: '40px',
padding: 0,
background: isSelected
? 'var(--md-sys-color-primary)'
: 'transparent',
border: isToday && !isSelected
? '1px solid var(--md-sys-color-primary)'
: 'none',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
fontWeight: isSelected ? '500' : '400',
color: isSelected
? 'var(--md-sys-color-on-primary)'
: isCurrentMonth
? 'var(--md-sys-color-on-surface)'
: 'var(--md-sys-color-on-surface-variant)',
opacity: isCurrentMonth ? 1 : 0.4,
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
if (!isSelected) {
e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)';
}
}}
onMouseLeave={(e) => {
if (!isSelected) {
e.currentTarget.style.background = 'transparent';
}
}}
>
{format(day, 'd')}
</button>
);
})}
</div>
{/* Today button */}
<div style={{
marginTop: '16px',
paddingTop: '16px',
borderTop: '1px solid var(--md-sys-color-outline-variant)',
display: 'flex',
justifyContent: 'center',
}}>
<button
type="button"
onClick={() => handleDateSelect(new Date())}
style={{
padding: '8px 16px',
background: 'transparent',
border: 'none',
borderRadius: '20px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
color: 'var(--md-sys-color-primary)',
transition: 'background 0.2s ease',
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--md-sys-color-primary-container)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
Сегодня
</button>
</div>
</div>
)}
</div>
);
};