feat: refactor NotificationTemplate and remove unused components for cleaner code

This commit is contained in:
Mo Tarbin
2026-07-02 20:53:12 -04:00
parent f11a1fd9cc
commit db0c199674
5 changed files with 93 additions and 473 deletions

View File

@@ -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',

View 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) }
}

View File

@@ -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

View File

@@ -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

View File

@@ -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