feat: refactor NotificationTemplate and remove unused components for cleaner code
This commit is contained in:
@@ -12,7 +12,7 @@ import Input from '@mui/joy/Input'
|
|||||||
import Option from '@mui/joy/Option'
|
import Option from '@mui/joy/Option'
|
||||||
import Select from '@mui/joy/Select'
|
import Select from '@mui/joy/Select'
|
||||||
import Typography from '@mui/joy/Typography'
|
import Typography from '@mui/joy/Typography'
|
||||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
|
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
|
||||||
import { TIME_UNITS } from '../utils/DurationUtils'
|
import { TIME_UNITS } from '../utils/DurationUtils'
|
||||||
|
|
||||||
@@ -512,6 +512,7 @@ const NotificationTemplate = ({
|
|||||||
'--Badge-fontSize': '0.7rem',
|
'--Badge-fontSize': '0.7rem',
|
||||||
'--Badge-paddingX': '5px',
|
'--Badge-paddingX': '5px',
|
||||||
top: 10,
|
top: 10,
|
||||||
|
left: 10,
|
||||||
'& .MuiBadge-badge': {
|
'& .MuiBadge-badge': {
|
||||||
background: colors.bgColor,
|
background: colors.bgColor,
|
||||||
color: 'white',
|
color: 'white',
|
||||||
|
|||||||
91
src/hooks/useFileUpload.js
Normal file
91
src/hooks/useFileUpload.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import imageCompression from 'browser-image-compression'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useUserProfile } from '../queries/UserQueries'
|
||||||
|
import { useNotification } from '../service/NotificationProvider'
|
||||||
|
import { apiClient } from '../utils/ApiClient'
|
||||||
|
import { isPlusAccount, resolvePhotoURL } from '../utils/Helpers'
|
||||||
|
|
||||||
|
export const useFileUpload = ({ entityType = 'chore_attachment', entityId } = {}) => {
|
||||||
|
const { showError } = useNotification()
|
||||||
|
const { data: userProfile } = useUserProfile()
|
||||||
|
|
||||||
|
const uploadFile = useCallback(
|
||||||
|
async file => {
|
||||||
|
if (!isPlusAccount(userProfile)) {
|
||||||
|
showError({
|
||||||
|
title: 'Plus Feature',
|
||||||
|
message:
|
||||||
|
'Image uploads are not available in the Basic plan. Upgrade to Plus to add images to your content.',
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const compressionOptions = {
|
||||||
|
maxSizeMB: entityType === 'profile' ? 0.5 : 1,
|
||||||
|
maxWidthOrHeight: entityType === 'profile' ? 320 : 1200,
|
||||||
|
useWebWorker: true,
|
||||||
|
fileType: 'image/jpeg',
|
||||||
|
}
|
||||||
|
|
||||||
|
const compressedFile = await imageCompression(file, compressionOptions)
|
||||||
|
const compressedJpegFile = new File(
|
||||||
|
[compressedFile],
|
||||||
|
`${file.name.split('.')[0]}.jpg`,
|
||||||
|
{ type: 'image/jpeg' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', compressedJpegFile)
|
||||||
|
formData.append('entityType', entityType)
|
||||||
|
if (entityId) formData.append('entityId', entityId)
|
||||||
|
|
||||||
|
const response = await apiClient.upload('/assets/chore', formData)
|
||||||
|
|
||||||
|
if (response.status === 507) {
|
||||||
|
showError({
|
||||||
|
title: 'Storage Quota Exceeded',
|
||||||
|
message: 'You have exceeded your quota for uploading files.',
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
} else if (response.status === 413) {
|
||||||
|
showError({
|
||||||
|
title: 'File Too Large',
|
||||||
|
message: 'The file you are trying to upload is too large.',
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
} else if (response.status === 403 && !isPlusAccount(userProfile)) {
|
||||||
|
showError({
|
||||||
|
title: 'Upgrade Required',
|
||||||
|
message: 'Image uploads are only available for Plus accounts.',
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
showError({
|
||||||
|
title: 'Permission Denied',
|
||||||
|
message: 'You do not have permission to upload files.',
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
} else if (!response.ok) {
|
||||||
|
showError({
|
||||||
|
title: 'Upload Failed',
|
||||||
|
message: 'Failed to upload image.',
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return resolvePhotoURL(data.url || data.sign)
|
||||||
|
} catch {
|
||||||
|
showError({
|
||||||
|
title: 'Upload Failed',
|
||||||
|
message: 'An error occurred while processing the image.',
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[entityType, entityId, showError, userProfile],
|
||||||
|
)
|
||||||
|
|
||||||
|
return { uploadFile, isPlus: isPlusAccount(userProfile) }
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Person } from '@mui/icons-material'
|
|
||||||
import BaseOptionPicker from './BaseOptionPicker'
|
|
||||||
|
|
||||||
const AssigneePickerPreview = ({
|
|
||||||
value = null,
|
|
||||||
onChange,
|
|
||||||
onClear,
|
|
||||||
members = [],
|
|
||||||
includeAnyone = true,
|
|
||||||
emptyDisplay,
|
|
||||||
}) => {
|
|
||||||
const options = [
|
|
||||||
...(includeAnyone ? [{ userId: 'anyone', displayName: 'Anyone' }] : []),
|
|
||||||
...members.map(member => ({
|
|
||||||
userId: member.userId,
|
|
||||||
displayName: member.displayName || member.username || 'Unknown',
|
|
||||||
})),
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseOptionPicker
|
|
||||||
items={options}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
onClear={onClear}
|
|
||||||
emptyDisplay={emptyDisplay}
|
|
||||||
emptyLabel='Assignee'
|
|
||||||
getItemValue={item => item.userId}
|
|
||||||
getItemLabel={item => item.displayName}
|
|
||||||
renderTriggerIcon={() => <Person sx={{ fontSize: '20px' }} />}
|
|
||||||
renderItemStart={() => <Person sx={{ fontSize: '18px' }} />}
|
|
||||||
getTriggerText={({ selectedItems, isEmpty }) =>
|
|
||||||
isEmpty ? 'Assignee' : selectedItems[0].displayName
|
|
||||||
}
|
|
||||||
menuMinWidth={220}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AssigneePickerPreview
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import { CalendarMonth, Close } from '@mui/icons-material'
|
|
||||||
import { Box, Button, IconButton, Input, Sheet, Typography } from '@mui/joy'
|
|
||||||
import { ClickAwayListener, Popper } from '@mui/material'
|
|
||||||
import moment from 'moment'
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { Z_INDEX } from '../../constants/zIndex'
|
|
||||||
|
|
||||||
const DueDatePickerPreview = ({
|
|
||||||
dueDateOnly,
|
|
||||||
dueTime,
|
|
||||||
useCustomTime,
|
|
||||||
onDueDateChange,
|
|
||||||
onDueTimeChange,
|
|
||||||
onUseCustomTimeChange,
|
|
||||||
onClear,
|
|
||||||
emptyDisplay = 'icon-text',
|
|
||||||
size = 'sm',
|
|
||||||
}) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const buttonRef = useRef(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return
|
|
||||||
|
|
||||||
const handleEscape = event => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setIsOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscape)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleEscape)
|
|
||||||
}
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
const hasDueDate = Boolean(dueDateOnly)
|
|
||||||
const shouldShowLabel = hasDueDate || emptyDisplay === 'icon-text'
|
|
||||||
|
|
||||||
const dueDateLabel = useMemo(() => {
|
|
||||||
if (!dueDateOnly) {
|
|
||||||
return 'Due'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedDate = moment(dueDateOnly).format('MMM D')
|
|
||||||
if (useCustomTime && dueTime) {
|
|
||||||
return `${formattedDate}, ${dueTime}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedDate
|
|
||||||
}, [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)}
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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>
|
|
||||||
<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)
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DueDatePickerPreview
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
import { Close, Repeat } from '@mui/icons-material'
|
|
||||||
import { Box, Button, IconButton, Sheet, Typography } from '@mui/joy'
|
|
||||||
import { ClickAwayListener, Popper } from '@mui/material'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { Z_INDEX } from '../../constants/zIndex'
|
|
||||||
import { getRecurrentChipText } from '../../utils/ChoreCardHelpers'
|
|
||||||
|
|
||||||
const REPEAT_PRESETS = [
|
|
||||||
{
|
|
||||||
id: 'daily',
|
|
||||||
label: 'Daily',
|
|
||||||
frequencyType: 'interval',
|
|
||||||
frequency: 1,
|
|
||||||
frequencyMetadata: { unit: 'days' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'weekly',
|
|
||||||
label: 'Weekly',
|
|
||||||
frequencyType: 'interval',
|
|
||||||
frequency: 1,
|
|
||||||
frequencyMetadata: { unit: 'weeks' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'monthly',
|
|
||||||
label: 'Monthly',
|
|
||||||
frequencyType: 'interval',
|
|
||||||
frequency: 1,
|
|
||||||
frequencyMetadata: { unit: 'months' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yearly',
|
|
||||||
label: 'Yearly',
|
|
||||||
frequencyType: 'interval',
|
|
||||||
frequency: 1,
|
|
||||||
frequencyMetadata: { unit: 'years' },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const matchPreset = value => {
|
|
||||||
if (!value) return null
|
|
||||||
return (
|
|
||||||
REPEAT_PRESETS.find(
|
|
||||||
p =>
|
|
||||||
p.frequencyType === value.frequencyType &&
|
|
||||||
p.frequency === value.frequency &&
|
|
||||||
p.frequencyMetadata?.unit === value.frequencyMetadata?.unit,
|
|
||||||
) || null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const RepeatPickerPreview = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onClear,
|
|
||||||
emptyDisplay = 'icon-text',
|
|
||||||
size = 'sm',
|
|
||||||
}) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const buttonRef = useRef(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return
|
|
||||||
const handleEscape = event => {
|
|
||||||
if (event.key === 'Escape') setIsOpen(false)
|
|
||||||
}
|
|
||||||
document.addEventListener('keydown', handleEscape)
|
|
||||||
return () => document.removeEventListener('keydown', handleEscape)
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
const hasRepeat = Boolean(value)
|
|
||||||
const selectedPreset = matchPreset(value)
|
|
||||||
const shouldShowLabel = hasRepeat || emptyDisplay === 'icon-text'
|
|
||||||
const displayLabel = hasRepeat ? getRecurrentChipText(value) : 'Repeat'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
||||||
<Button
|
|
||||||
ref={buttonRef}
|
|
||||||
size={size}
|
|
||||||
variant={hasRepeat ? 'soft' : 'outlined'}
|
|
||||||
color='neutral'
|
|
||||||
onClick={() => setIsOpen(prev => !prev)}
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Repeat 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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{displayLabel}
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{hasRepeat && 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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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: 180,
|
|
||||||
p: 0.75,
|
|
||||||
borderRadius: 'md',
|
|
||||||
boxShadow: 'lg',
|
|
||||||
bgcolor: 'background.popup',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{REPEAT_PRESETS.map((preset, index) => {
|
|
||||||
const isSelected = selectedPreset?.id === preset.id
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={preset.id}
|
|
||||||
variant={isSelected ? 'soft' : 'plain'}
|
|
||||||
color='neutral'
|
|
||||||
onClick={() => {
|
|
||||||
onChange({
|
|
||||||
frequencyType: preset.frequencyType,
|
|
||||||
frequency: preset.frequency,
|
|
||||||
frequencyMetadata: preset.frequencyMetadata,
|
|
||||||
})
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
mb: index === REPEAT_PRESETS.length - 1 ? 0 : 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography level='body-sm'>
|
|
||||||
{getRecurrentChipText(preset)}
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
{hasRepeat && (
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='plain'
|
|
||||||
color='neutral'
|
|
||||||
onClick={() => {
|
|
||||||
onClear?.()
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
sx={{ width: '100%', mt: 0.5, justifyContent: 'flex-start' }}
|
|
||||||
>
|
|
||||||
No repeat
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Sheet>
|
|
||||||
</ClickAwayListener>
|
|
||||||
</Popper>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RepeatPickerPreview
|
|
||||||
Reference in New Issue
Block a user