uchill/front_material/components/common/DatePicker.tsx

317 lines
9.2 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 — Dialog variant.
* Opens a calendar inside a MUI Dialog (works well on mobile and inside other dialogs).
*/
'use client';
import React, { useState, useMemo } from 'react';
import {
format,
startOfMonth,
endOfMonth,
eachDayOfInterval,
isSameDay,
isSameMonth,
addMonths,
subMonths,
startOfWeek,
endOfWeek,
} from 'date-fns';
import { ru } from 'date-fns/locale';
import { Dialog, DialogContent, Box, Button } from '@mui/material';
interface DatePickerProps {
value: string; // YYYY-MM-DD format
onChange: (value: string) => void;
disabled?: boolean;
required?: boolean;
label?: string;
}
export const DatePicker: React.FC<DatePickerProps> = ({
value,
onChange,
disabled = false,
required = false,
label,
}) => {
const [open, setOpen] = useState(false);
const [displayMonth, setDisplayMonth] = useState(
value ? new Date(value + 'T00:00:00') : new Date(),
);
const selectedDate = useMemo(
() => (value ? new Date(value + 'T00:00:00') : null),
[value],
);
const openPicker = () => {
if (disabled) return;
setDisplayMonth(selectedDate ?? new Date());
setOpen(true);
};
const closePicker = () => setOpen(false);
const handleDateSelect = (date: Date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
onChange(`${y}-${m}-${d}`);
closePicker();
};
const days = useMemo(() => {
const start = startOfMonth(displayMonth);
const end = endOfMonth(displayMonth);
return eachDayOfInterval({
start: startOfWeek(start, { locale: ru }),
end: endOfWeek(end, { locale: ru }),
});
}, [displayMonth]);
const weekDays = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
const displayValue = selectedDate
? format(selectedDate, 'd MMMM yyyy', { locale: ru })
: label || 'Выберите дату';
return (
<>
<button
type="button"
onClick={openPicker}
disabled={disabled}
aria-required={required}
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 var(--md-sys-color-outline)',
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>{displayValue}</span>
<span
className="material-symbols-outlined"
style={{ fontSize: 20, opacity: 0.7 }}
>
calendar_today
</span>
</button>
<Dialog
open={open}
onClose={closePicker}
fullWidth
maxWidth="xs"
slotProps={{
paper: {
sx: {
borderRadius: '24px',
overflow: 'visible',
bgcolor: 'var(--md-sys-color-surface)',
},
},
}}
>
<DialogContent sx={{ p: 2 }}>
{/* Month/year header */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 1.5,
}}
>
<button
type="button"
onClick={() => setDisplayMonth(subMonths(displayMonth, 1))}
style={{
width: 36,
height: 36,
padding: 0,
background: 'transparent',
border: 'none',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface)',
}}
>
<span className="material-symbols-outlined" style={{ fontSize: 22 }}>
chevron_left
</span>
</button>
<span
style={{
fontSize: 16,
fontWeight: 500,
color: 'var(--md-sys-color-on-surface)',
textTransform: 'capitalize',
}}
>
{format(displayMonth, 'LLLL yyyy', { locale: ru })}
</span>
<button
type="button"
onClick={() => setDisplayMonth(addMonths(displayMonth, 1))}
style={{
width: 36,
height: 36,
padding: 0,
background: 'transparent',
border: 'none',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface)',
}}
>
<span className="material-symbols-outlined" style={{ fontSize: 22 }}>
chevron_right
</span>
</button>
</Box>
{/* Weekday headers */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 2,
marginBottom: 4,
}}
>
{weekDays.map((day) => (
<div
key={day}
style={{
textAlign: 'center',
fontSize: 12,
fontWeight: 500,
color: 'var(--md-sys-color-on-surface-variant)',
padding: '6px 0',
}}
>
{day}
</div>
))}
</div>
{/* Calendar days */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 2,
}}
>
{days.map((day, idx) => {
const isSelected = selectedDate && isSameDay(day, selectedDate);
const isCurrent = isSameMonth(day, displayMonth);
const isToday = isSameDay(day, new Date());
return (
<button
key={idx}
type="button"
onClick={() => handleDateSelect(day)}
style={{
width: '100%',
aspectRatio: '1',
maxWidth: 40,
margin: '0 auto',
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: 14,
fontWeight: isSelected ? 600 : 400,
color: isSelected
? 'var(--md-sys-color-on-primary)'
: isCurrent
? 'var(--md-sys-color-on-surface)'
: 'var(--md-sys-color-on-surface-variant)',
opacity: isCurrent ? 1 : 0.35,
transition: 'background 0.15s',
}}
>
{format(day, 'd')}
</button>
);
})}
</div>
{/* Actions */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mt: 2,
pt: 1.5,
borderTop: '1px solid var(--md-sys-color-outline-variant)',
}}
>
<Button
onClick={() => handleDateSelect(new Date())}
variant="text"
sx={{
color: 'var(--md-sys-color-primary)',
textTransform: 'none',
fontWeight: 500,
fontSize: 14,
}}
>
Сегодня
</Button>
<Button
onClick={closePicker}
variant="text"
sx={{
color: 'var(--md-sys-color-on-surface-variant)',
textTransform: 'none',
fontWeight: 500,
fontSize: 14,
}}
>
Отмена
</Button>
</Box>
</DialogContent>
</Dialog>
</>
);
};