feat: enhance DueDatePickerField with quick date and time selection options
This commit is contained in:
@@ -1,16 +1,31 @@
|
||||
import {
|
||||
Bedtime,
|
||||
CalendarMonth,
|
||||
Close,
|
||||
EventNote,
|
||||
LightMode,
|
||||
NextWeek,
|
||||
NightsStay,
|
||||
Today,
|
||||
WbSunny,
|
||||
WbTwilight,
|
||||
Weekend,
|
||||
} from '@mui/icons-material'
|
||||
import { Box, Button, IconButton, Input, Sheet, Typography } from '@mui/joy'
|
||||
import { ClickAwayListener, Popper } from '@mui/material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
IconButton,
|
||||
Input,
|
||||
List,
|
||||
ListItem,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Z_INDEX } from '../../constants/zIndex'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Calendar from 'react-calendar'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
||||
|
||||
const DueDatePickerField = ({
|
||||
dueDateOnly,
|
||||
@@ -24,7 +39,34 @@ const DueDatePickerField = ({
|
||||
size = 'sm',
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const buttonRef = useRef(null)
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const { firstDayOfWeek } = useLocalization()
|
||||
|
||||
// Local buffered state — only committed on Apply
|
||||
const [localDueDateOnly, setLocalDueDateOnly] = useState(dueDateOnly)
|
||||
const [localDueTime, setLocalDueTime] = useState(dueTime)
|
||||
const [localUseCustomTime, setLocalUseCustomTime] = useState(useCustomTime)
|
||||
|
||||
// Sync local state from props whenever the modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLocalDueDateOnly(dueDateOnly)
|
||||
setLocalDueTime(dueTime)
|
||||
setLocalUseCustomTime(useCustomTime)
|
||||
}
|
||||
}, [isOpen, dueDateOnly, dueTime, useCustomTime])
|
||||
|
||||
const calendarType =
|
||||
firstDayOfWeek === 1
|
||||
? 'iso8601'
|
||||
: firstDayOfWeek === 6
|
||||
? 'islamic'
|
||||
: 'gregory'
|
||||
|
||||
const pillListSx = {
|
||||
'--List-gap': '8px',
|
||||
'--ListItem-radius': '20px',
|
||||
}
|
||||
|
||||
const getQuickScheduleDate = option => {
|
||||
const now = new Date()
|
||||
@@ -50,6 +92,11 @@ const DueDatePickerField = ({
|
||||
nextWeek.setDate(today.getDate() + daysUntilMonday)
|
||||
return nextWeek
|
||||
}
|
||||
case 'next-month': {
|
||||
const nextMonth = new Date(today)
|
||||
nextMonth.setMonth(today.getMonth() + 1)
|
||||
return nextMonth
|
||||
}
|
||||
default:
|
||||
return today
|
||||
}
|
||||
@@ -57,25 +104,43 @@ const DueDatePickerField = ({
|
||||
|
||||
const handleQuickSchedule = option => {
|
||||
const date = getQuickScheduleDate(option)
|
||||
const dateStr = date.toISOString().split('T')[0]
|
||||
onDueDateChange?.({ target: { value: dateStr } })
|
||||
setIsOpen(false)
|
||||
setLocalDueDateOnly(date.toISOString().split('T')[0])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
const handleEscape = event => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
}
|
||||
const handleQuickTime = timeStr => {
|
||||
// Tap the active chip again to deselect it
|
||||
if (localUseCustomTime && localDueTime === timeStr) {
|
||||
setLocalUseCustomTime(false)
|
||||
setLocalDueTime(null)
|
||||
return
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
if (!localDueDateOnly) {
|
||||
setLocalDueDateOnly(new Date().toISOString().split('T')[0])
|
||||
}
|
||||
}, [isOpen])
|
||||
setLocalUseCustomTime(true)
|
||||
setLocalDueTime(timeStr)
|
||||
}
|
||||
|
||||
const handleCalendarChange = selected => {
|
||||
if (!selected || Array.isArray(selected)) return
|
||||
setLocalDueDateOnly(moment(selected).format('YYYY-MM-DD'))
|
||||
}
|
||||
|
||||
const handleLocalTimeInputChange = e => {
|
||||
setLocalUseCustomTime(true)
|
||||
setLocalDueTime(e.target.value)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onDueDateChange?.({ target: { value: localDueDateOnly || '' } })
|
||||
onUseCustomTimeChange?.(localUseCustomTime)
|
||||
if (localUseCustomTime && localDueTime) {
|
||||
onDueTimeChange?.({ target: { value: localDueTime } })
|
||||
} else {
|
||||
onDueTimeChange?.({ target: { value: '' } })
|
||||
}
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const hasDueDate = Boolean(dueDateOnly)
|
||||
const shouldShowLabel = hasDueDate || emptyDisplay === 'icon-text'
|
||||
@@ -94,210 +159,469 @@ const DueDatePickerField = ({
|
||||
}, [dueDateOnly, dueTime, useCustomTime])
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
size={size}
|
||||
variant={hasDueDate ? 'soft' : 'outlined'}
|
||||
color='neutral'
|
||||
onClick={() => setIsOpen(prev => !prev)}
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 40,
|
||||
borderRadius: '128px',
|
||||
minWidth: 'min-content',
|
||||
px: shouldShowLabel ? 1.25 : 0.75,
|
||||
gap: shouldShowLabel ? 1 : 0,
|
||||
justifyContent: 'flex-start',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.25s ease-in-out',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CalendarMonth sx={{ fontSize: '20px' }} />
|
||||
<Typography
|
||||
level='body-sm'
|
||||
<Button
|
||||
size={size}
|
||||
variant={hasDueDate ? 'soft' : 'outlined'}
|
||||
color='neutral'
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{
|
||||
minHeight: 40,
|
||||
borderRadius: '128px',
|
||||
minWidth: 'min-content',
|
||||
px: shouldShowLabel ? 1.25 : 0.75,
|
||||
gap: shouldShowLabel ? 1 : 0,
|
||||
justifyContent: 'flex-start',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: shouldShowLabel ? 220 : 0,
|
||||
opacity: shouldShowLabel ? 1 : 0,
|
||||
transform: shouldShowLabel ? 'translateX(0)' : 'translateX(-4px)',
|
||||
transition:
|
||||
'max-width 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.25s ease-in-out',
|
||||
transition: 'all 0.25s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{dueDateLabel}
|
||||
</Typography>
|
||||
</Button>
|
||||
{hasDueDate && onClear && (
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onClear?.()
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -12,
|
||||
right: -16,
|
||||
zIndex: 10,
|
||||
maxHeight: 18,
|
||||
maxWidth: 18,
|
||||
borderRadius: '50%',
|
||||
'&:hover': {
|
||||
bgcolor: 'danger.softBg',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Close sx={{ fontSize: '18px' }} />
|
||||
</IconButton>
|
||||
)}
|
||||
<CalendarMonth sx={{ fontSize: '20px' }} />
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: shouldShowLabel ? 220 : 0,
|
||||
opacity: shouldShowLabel ? 1 : 0,
|
||||
transform: shouldShowLabel ? 'translateX(0)' : 'translateX(-4px)',
|
||||
transition:
|
||||
'max-width 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.25s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{dueDateLabel}
|
||||
</Typography>
|
||||
</Button>
|
||||
{hasDueDate && onClear && (
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onClear?.()
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -12,
|
||||
right: -16,
|
||||
zIndex: 10,
|
||||
maxHeight: 18,
|
||||
maxWidth: 18,
|
||||
borderRadius: '50%',
|
||||
'&:hover': {
|
||||
bgcolor: 'danger.softBg',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Close sx={{ fontSize: '18px' }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isOpen && (
|
||||
<Popper
|
||||
open={isOpen}
|
||||
anchorEl={buttonRef.current}
|
||||
placement='top-start'
|
||||
modifiers={[
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
fallbackPlacements: ['bottom-start', 'top-start'],
|
||||
},
|
||||
},
|
||||
]}
|
||||
sx={{ zIndex: Z_INDEX.MODAL_CLOSE_BUTTON + 1 }}
|
||||
>
|
||||
<ClickAwayListener onClickAway={() => setIsOpen(false)}>
|
||||
<Sheet
|
||||
variant='outlined'
|
||||
sx={{
|
||||
minWidth: 260,
|
||||
p: 1,
|
||||
borderRadius: 'md',
|
||||
boxShadow: 'lg',
|
||||
bgcolor: 'background.popup',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
||||
Due Date
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ display: 'flex', gap: 0.5, mb: 1.25, flexWrap: 'wrap' }}
|
||||
>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
startDecorator={<Today sx={{ fontSize: 16 }} />}
|
||||
onClick={() => handleQuickSchedule('today')}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
startDecorator={<WbSunny sx={{ fontSize: 16 }} />}
|
||||
onClick={() => handleQuickSchedule('tomorrow')}
|
||||
>
|
||||
Tomorrow
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
startDecorator={<Weekend sx={{ fontSize: 16 }} />}
|
||||
onClick={() => handleQuickSchedule('weekend')}
|
||||
>
|
||||
Weekend
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
startDecorator={<NextWeek sx={{ fontSize: 16 }} />}
|
||||
onClick={() => handleQuickSchedule('next-week')}
|
||||
>
|
||||
Next week
|
||||
</Button>
|
||||
</Box>
|
||||
<Input
|
||||
type='date'
|
||||
value={dueDateOnly || ''}
|
||||
onChange={onDueDateChange}
|
||||
sx={{ mb: 1 }}
|
||||
/>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ mb: 0.5, color: 'text.tertiary' }}
|
||||
>
|
||||
Due time (optional)
|
||||
</Typography>
|
||||
<Input
|
||||
type='time'
|
||||
value={dueTime || ''}
|
||||
disabled={!dueDateOnly}
|
||||
onChange={e => {
|
||||
if (!useCustomTime) {
|
||||
onUseCustomTimeChange?.(true)
|
||||
}
|
||||
onDueTimeChange?.(e)
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
title='Due Date'
|
||||
footer={
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
{hasDueDate && (
|
||||
<Button
|
||||
variant='plain'
|
||||
color='danger'
|
||||
size='lg'
|
||||
onClick={() => {
|
||||
onClear?.()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
sx={{ maxWidth: 200, mb: 1 }}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 0.75, mb: 0.5 }}>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={!useCustomTime ? 'soft' : 'plain'}
|
||||
color='neutral'
|
||||
disabled={!dueDateOnly}
|
||||
onClick={() => onUseCustomTimeChange?.(false)}
|
||||
>
|
||||
Anytime
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={useCustomTime ? 'soft' : 'plain'}
|
||||
color='neutral'
|
||||
disabled={!dueDateOnly}
|
||||
onClick={() => onUseCustomTimeChange?.(true)}
|
||||
>
|
||||
Specific time
|
||||
</Button>
|
||||
</Box>
|
||||
{hasDueDate && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
onClick={() => {
|
||||
onClear?.()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
Clear due date
|
||||
</Button>
|
||||
)}
|
||||
</Sheet>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
)}
|
||||
</Box>
|
||||
sx={{ mr: 'auto' }}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
size='lg'
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
size='lg'
|
||||
onClick={handleSave}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box sx={{ fontFamily: 'var(--joy-fontFamily-body)' }}>
|
||||
{/* Date shortcuts */}
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
mb: 0.75,
|
||||
color: 'text.tertiary',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
Quick date
|
||||
</Typography>
|
||||
<List orientation='horizontal' wrap sx={{ ...pillListSx, mb: 1.5 }}>
|
||||
{[
|
||||
{
|
||||
key: 'today',
|
||||
label: 'Today',
|
||||
icon: <Today sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
key: 'tomorrow',
|
||||
label: 'Tomorrow',
|
||||
icon: <WbSunny sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
key: 'weekend',
|
||||
label: 'Weekend',
|
||||
icon: <Weekend sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
key: 'next-week',
|
||||
label: 'Next week',
|
||||
icon: <NextWeek sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
key: 'next-month',
|
||||
label: 'Next month',
|
||||
icon: <EventNote sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
].map(opt => {
|
||||
const dateStr = getQuickScheduleDate(opt.key)
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
return (
|
||||
<ListItem key={opt.key}>
|
||||
<Checkbox
|
||||
checked={localDueDateOnly === dateStr}
|
||||
onClick={() => handleQuickSchedule(opt.key)}
|
||||
overlay
|
||||
disableIcon
|
||||
variant='soft'
|
||||
label={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
{opt.icon}
|
||||
{opt.label}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
|
||||
{/* Time shortcuts */}
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
mb: 0.75,
|
||||
color: 'text.tertiary',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
Quick time
|
||||
</Typography>
|
||||
<List orientation='horizontal' wrap sx={{ ...pillListSx, mb: 1.5 }}>
|
||||
{[
|
||||
{
|
||||
time: '09:00',
|
||||
label: 'Morning',
|
||||
icon: <LightMode sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
time: '12:00',
|
||||
label: 'Noon',
|
||||
icon: <WbSunny sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
time: '15:00',
|
||||
label: 'Afternoon',
|
||||
icon: <WbTwilight sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
time: '18:00',
|
||||
label: 'Evening',
|
||||
icon: <NightsStay sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
{
|
||||
time: '22:00',
|
||||
label: 'Night',
|
||||
icon: <Bedtime sx={{ fontSize: 14 }} />,
|
||||
},
|
||||
].map(opt => (
|
||||
<ListItem key={opt.time}>
|
||||
<Checkbox
|
||||
checked={localUseCustomTime && localDueTime === opt.time}
|
||||
onClick={() => handleQuickTime(opt.time)}
|
||||
overlay
|
||||
disableIcon
|
||||
variant='soft'
|
||||
label={
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
||||
>
|
||||
{opt.icon}
|
||||
{opt.label}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
bgcolor: 'background.surface',
|
||||
p: 1,
|
||||
// Fix the height so switching views (month/year/decade) doesn't
|
||||
// cause layout shift — month view with 6 rows is the tallest.
|
||||
minHeight: 300,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& .react-calendar': {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
'& .react-calendar__viewContainer': {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
'& .react-calendar__month-view, & .react-calendar__year-view, & .react-calendar__decade-view, & .react-calendar__century-view':
|
||||
{
|
||||
flex: 1,
|
||||
},
|
||||
// Navigation row
|
||||
'& .react-calendar__navigation': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
mb: 1,
|
||||
},
|
||||
// All nav buttons — large tap targets
|
||||
'& .react-calendar__navigation button': {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: 'var(--joy-palette-text-primary)',
|
||||
fontFamily: 'var(--joy-fontFamily-body)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
minHeight: '40px',
|
||||
minWidth: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 8px',
|
||||
transition: 'background 0.15s',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softBg)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.35,
|
||||
cursor: 'default',
|
||||
},
|
||||
},
|
||||
// Label button (month/year text) takes remaining space
|
||||
'& .react-calendar__navigation__label': {
|
||||
flex: 1,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.01em',
|
||||
},
|
||||
// Prev/next arrow buttons — slightly larger icon feel
|
||||
'& .react-calendar__navigation__prev-button, & .react-calendar__navigation__next-button':
|
||||
{
|
||||
fontSize: '1.75rem',
|
||||
},
|
||||
'& .react-calendar__navigation__prev2-button, & .react-calendar__navigation__next2-button':
|
||||
{
|
||||
fontSize: '1.4rem',
|
||||
},
|
||||
// Weekday headers
|
||||
'& .react-calendar__month-view__weekdays__weekday': {
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
color: 'var(--joy-palette-text-tertiary)',
|
||||
textAlign: 'center',
|
||||
padding: '4px 0',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.04em',
|
||||
},
|
||||
'& .react-calendar__month-view__weekdays__weekday abbr': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
// All tiles — shared base
|
||||
'& .react-calendar__tile': {
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
color: 'var(--joy-palette-text-primary)',
|
||||
fontFamily: 'var(--joy-fontFamily-body)',
|
||||
fontSize: '0.8rem',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'background 0.15s',
|
||||
'&:hover': {
|
||||
background: 'var(--joy-palette-neutral-softBg)',
|
||||
},
|
||||
},
|
||||
// Day tiles only — circular
|
||||
'& .react-calendar__month-view__days .react-calendar__tile': {
|
||||
aspectRatio: '1',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
// Month tiles (year view) — pill shape, no huge circle
|
||||
'& .react-calendar__year-view .react-calendar__tile': {
|
||||
borderRadius: '8px',
|
||||
padding: '10px 4px',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
// Year tiles (decade view) — pill shape
|
||||
'& .react-calendar__decade-view .react-calendar__tile': {
|
||||
borderRadius: '8px',
|
||||
padding: '10px 4px',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
// Century tiles — pill shape
|
||||
'& .react-calendar__century-view .react-calendar__tile': {
|
||||
borderRadius: '8px',
|
||||
padding: '10px 4px',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
'& .react-calendar__tile--now': {
|
||||
border:
|
||||
'1.5px solid var(--joy-palette-primary-solidBg) !important',
|
||||
color: 'var(--joy-palette-primary-solidBg) !important',
|
||||
fontWeight: 700,
|
||||
background: 'none !important',
|
||||
},
|
||||
'& .react-calendar__tile--active, & .react-calendar__tile--active:hover':
|
||||
{
|
||||
background: 'var(--joy-palette-primary-solidBg) !important',
|
||||
color: 'var(--joy-palette-primary-solidColor) !important',
|
||||
fontWeight: 700,
|
||||
},
|
||||
'& .react-calendar__month-view__days__day--neighboringMonth': {
|
||||
color: 'var(--joy-palette-text-tertiary)',
|
||||
},
|
||||
'& .react-calendar__month-view__days': {
|
||||
display: 'grid !important',
|
||||
gridTemplateColumns: 'repeat(7, 1fr) !important',
|
||||
},
|
||||
'& .react-calendar__month-view__weekdays': {
|
||||
display: 'grid !important',
|
||||
gridTemplateColumns: 'repeat(7, 1fr) !important',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Calendar
|
||||
value={
|
||||
localDueDateOnly
|
||||
? new Date(`${localDueDateOnly}T00:00:00`)
|
||||
: null
|
||||
}
|
||||
calendarType={calendarType}
|
||||
onChange={handleCalendarChange}
|
||||
formatShortWeekday={(locale, date) =>
|
||||
['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'][date.getDay()]
|
||||
}
|
||||
formatMonth={(locale, date) =>
|
||||
[
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
][date.getMonth()]
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
mt: 0.5,
|
||||
color: 'text.tertiary',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
Custom time
|
||||
</Typography>
|
||||
<Input
|
||||
type='time'
|
||||
size='sm'
|
||||
value={localUseCustomTime ? localDueTime || '' : ''}
|
||||
disabled={!localDueDateOnly}
|
||||
onChange={handleLocalTimeInputChange}
|
||||
sx={{ maxWidth: 200, mb: 1 }}
|
||||
slotProps={{ input: { style: { fontFamily: 'inherit' } } }}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 0.75, mb: 0.5 }}>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={!localUseCustomTime ? 'soft' : 'plain'}
|
||||
color='neutral'
|
||||
disabled={!localDueDateOnly}
|
||||
onClick={() => setLocalUseCustomTime(false)}
|
||||
>
|
||||
Anytime
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={localUseCustomTime ? 'soft' : 'plain'}
|
||||
color='neutral'
|
||||
disabled={!localDueDateOnly}
|
||||
onClick={() => setLocalUseCustomTime(true)}
|
||||
>
|
||||
Specific time
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user