159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
/**
|
||
* TimePicker на базе MUI X: Static variant (как на скрине).
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import React from 'react';
|
||
import { ru } from 'date-fns/locale';
|
||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||
import { StaticTimePicker } from '@mui/x-date-pickers/StaticTimePicker';
|
||
import { Box, Dialog, DialogContent, DialogTitle, Button } from '@mui/material';
|
||
|
||
interface TimePickerProps {
|
||
value: string; // HH:mm format
|
||
onChange: (value: string) => void;
|
||
disabled?: boolean;
|
||
required?: boolean;
|
||
}
|
||
|
||
export const TimePicker: React.FC<TimePickerProps> = ({
|
||
value,
|
||
onChange,
|
||
disabled = false,
|
||
required = false,
|
||
}) => {
|
||
const [open, setOpen] = React.useState(false);
|
||
const [draft, setDraft] = React.useState<Date | null>(null);
|
||
|
||
const valueDate = React.useMemo(() => {
|
||
const base = new Date();
|
||
const parts = (value || '').split(':');
|
||
const hh = Number(parts[0]);
|
||
const mm = Number(parts[1]);
|
||
if (Number.isFinite(hh) && Number.isFinite(mm)) {
|
||
base.setHours(hh, mm, 0, 0);
|
||
return base;
|
||
}
|
||
base.setHours(0, 0, 0, 0);
|
||
return base;
|
||
}, [value]);
|
||
|
||
const openPicker = () => {
|
||
if (disabled) return;
|
||
setDraft(valueDate);
|
||
setOpen(true);
|
||
};
|
||
|
||
const closePicker = () => setOpen(false);
|
||
|
||
const commit = () => {
|
||
const d = draft ?? valueDate;
|
||
const hh = String(d.getHours()).padStart(2, '0');
|
||
const mm = String(d.getMinutes()).padStart(2, '0');
|
||
onChange(`${hh}:${mm}`);
|
||
closePicker();
|
||
};
|
||
|
||
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>{value || 'Выберите время'}</span>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<circle cx="12" cy="12" r="10"></circle>
|
||
<polyline points="12 6 12 12 16 14"></polyline>
|
||
</svg>
|
||
</button>
|
||
|
||
<Dialog open={open} onClose={closePicker} fullWidth maxWidth="xs">
|
||
<DialogTitle sx={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.08em' }}>
|
||
ВЫБЕРИТЕ ВРЕМЯ
|
||
</DialogTitle>
|
||
<DialogContent sx={{ pt: 1 }}>
|
||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ru}>
|
||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||
<StaticTimePicker
|
||
value={draft}
|
||
onChange={(v) => setDraft(v == null ? null : v instanceof Date ? v : (v as { toDate?: () => Date }).toDate?.() ?? null)}
|
||
ampm={false}
|
||
minutesStep={5}
|
||
localeText={{}}
|
||
sx={{
|
||
'& .MuiClockPointer-root': {
|
||
backgroundColor: 'var(--md-sys-color-primary)',
|
||
},
|
||
'& .MuiClockPointer-thumb': {
|
||
borderColor: 'var(--md-sys-color-primary)',
|
||
backgroundColor: 'var(--md-sys-color-primary)',
|
||
},
|
||
'& .MuiClock-pin': {
|
||
backgroundColor: 'var(--md-sys-color-primary)',
|
||
},
|
||
'& .MuiClockNumber-root.Mui-selected': {
|
||
backgroundColor: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
},
|
||
}}
|
||
slotProps={{
|
||
// убираем встроенные CANCEL/OK у MUI
|
||
actionBar: { actions: [] },
|
||
}}
|
||
/>
|
||
</Box>
|
||
</LocalizationProvider>
|
||
|
||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 1.5 }}>
|
||
<Button
|
||
onClick={closePicker}
|
||
variant="text"
|
||
sx={{
|
||
color: 'var(--md-sys-color-primary)',
|
||
'&:hover': { backgroundColor: 'rgba(116, 68, 253, 0.08)' },
|
||
}}
|
||
>
|
||
ОТМЕНА
|
||
</Button>
|
||
<Button
|
||
onClick={commit}
|
||
variant="contained"
|
||
sx={{
|
||
backgroundColor: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
'&:hover': {
|
||
backgroundColor: 'color-mix(in srgb, var(--md-sys-color-primary) 88%, #000 12%)',
|
||
},
|
||
}}
|
||
>
|
||
ОК
|
||
</Button>
|
||
</Box>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
};
|