feat: Refactor user profile management and improve token validation
- Introduced `useUserProfile` hook to centralize user profile fetching and management. - Updated various components to utilize the new `useUserProfile` hook instead of context. - Enhanced token validation logic in `Fetch` function to prevent unnecessary redirects. - Added `parseDueDate` function to handle due date parsing with improved logic. - Cleaned up user profile state management across multiple views and settings. - Improved loading states and error handling in user-related components. - remove usercontext and just use react query for userProfile
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Save } from '@mui/icons-material'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import InfoIcon from '@mui/icons-material/Info'
|
||||
@@ -14,66 +15,64 @@ import Typography from '@mui/joy/Typography'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const timeUnits = [
|
||||
{ label: 'Minutes', value: 'minutes' },
|
||||
{ label: 'Mins', value: 'minutes' },
|
||||
{ label: 'Hours', value: 'hours' },
|
||||
{ label: 'Days', value: 'days' },
|
||||
]
|
||||
|
||||
const beforeAfterOptions = [
|
||||
{ label: 'Before Due', value: 'before' },
|
||||
{ label: 'Before', value: 'before' },
|
||||
{ label: 'On Due', value: 'ondue' },
|
||||
{ label: 'After Due', value: 'after' },
|
||||
{ label: 'After', value: 'after' },
|
||||
]
|
||||
|
||||
function getRelativeLabel(notification) {
|
||||
const { amount, unit, when } = notification
|
||||
|
||||
// For "On Due" notification
|
||||
if (when === 'ondue') {
|
||||
const { value, unit, type } = notification
|
||||
if (type === 'ondue') {
|
||||
return 'On due date'
|
||||
}
|
||||
|
||||
// For before/after notifications
|
||||
return `${amount} ${unit} ${when === 'before' ? 'before' : 'after'} due`
|
||||
return `${value} ${unit} ${type === 'before' ? 'before' : 'after'} due`
|
||||
}
|
||||
|
||||
const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
const NotificationTemplate = ({
|
||||
maxNotifications = 5,
|
||||
onChange,
|
||||
value,
|
||||
showTimeline = true,
|
||||
}) => {
|
||||
const [templateName, setTemplateName] = useState(
|
||||
value?.name || 'New Notification Template',
|
||||
)
|
||||
const [notifications, setNotifications] = useState([
|
||||
value?.notifications || {
|
||||
amount: 0,
|
||||
when: 'ondue',
|
||||
},
|
||||
])
|
||||
const [error, setError] = useState(null)
|
||||
const [notifications, setNotifications] = useState(
|
||||
value?.templates ||
|
||||
JSON.parse(localStorage.getItem('defaultNotificationTemplate')) ||
|
||||
[],
|
||||
)
|
||||
|
||||
const [error, setError] = useState(null)
|
||||
const [showSaveDefault, setShowSaveDefault] = useState(false)
|
||||
// Create a map of notification indices for timeline display
|
||||
const [notificationIndexMap, setNotificationIndexMap] = useState({})
|
||||
|
||||
// Use useCallback to memoize the function
|
||||
const updateNotificationIndices = useCallback(() => {
|
||||
// Sort notifications for consistent ordering
|
||||
const sorted = [...notifications].sort((a, b) => {
|
||||
// Always ensure correct ordering: Before Due -> On Due -> After Due
|
||||
if (a.when !== b.when) {
|
||||
// Before Due always comes first
|
||||
if (a.when === 'before') return -1
|
||||
if (b.when === 'before') return 1
|
||||
if (a.type !== b.type) {
|
||||
// Before Due first
|
||||
if (a.type === 'before') return -1
|
||||
if (b.type === 'before') return 1
|
||||
|
||||
// On Due comes before After Due
|
||||
if (a.when === 'ondue') return -1
|
||||
if (b.when === 'ondue') return 1
|
||||
|
||||
// Default case (should not be reached with our options)
|
||||
if (a.type === 'ondue') return -1
|
||||
if (b.type === 'ondue') return 1
|
||||
// DEFAULT CASE ( NOT SURE FOR FUTURE )?
|
||||
return 0
|
||||
}
|
||||
|
||||
// Convert everything to minutes for consistent comparison
|
||||
const getMinutes = notif => {
|
||||
const { amount, unit } = notif
|
||||
let minutes = amount
|
||||
const { value, unit } = notif
|
||||
let minutes = value
|
||||
if (unit === 'hours') minutes *= 60
|
||||
if (unit === 'days') minutes *= 24 * 60
|
||||
return minutes
|
||||
@@ -83,17 +82,16 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
// For After Due: sort in ascending order (closest to due first)
|
||||
const aMinutes = getMinutes(a)
|
||||
const bMinutes = getMinutes(b)
|
||||
return a.when === 'before' ? bMinutes - aMinutes : aMinutes - bMinutes
|
||||
return a.type === 'before' ? bMinutes - aMinutes : aMinutes - bMinutes
|
||||
})
|
||||
|
||||
// Create index mapping
|
||||
const indexMap = {}
|
||||
sorted.forEach((item, index) => {
|
||||
const originalIdx = notifications.findIndex(
|
||||
n =>
|
||||
n.amount === item.amount &&
|
||||
n.value === item.value &&
|
||||
n.unit === item.unit &&
|
||||
n.when === item.when,
|
||||
n.type === item.type,
|
||||
)
|
||||
indexMap[originalIdx] = index + 1
|
||||
})
|
||||
@@ -104,27 +102,25 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
// Sort notifications and update the index mapping
|
||||
useEffect(() => {
|
||||
updateNotificationIndices()
|
||||
// Clear any errors when notifications change
|
||||
setError(null)
|
||||
}, [updateNotificationIndices])
|
||||
|
||||
// Notify parent component of changes including the template name
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange({ name: templateName, notifications })
|
||||
onChange({ notifications })
|
||||
}
|
||||
}, [templateName, notifications, onChange])
|
||||
}, [notifications, onChange])
|
||||
|
||||
// Validates if a notification configuration already exists
|
||||
const isDuplicate = (notification, currentIdx = -1) => {
|
||||
return notifications.some((n, idx) => {
|
||||
// Skip comparing with itself when editing
|
||||
if (idx === currentIdx) return false
|
||||
|
||||
return (
|
||||
n.amount === notification.amount &&
|
||||
n.value === notification.value &&
|
||||
n.unit === notification.unit &&
|
||||
n.when === notification.when
|
||||
n.type === notification.type
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -136,17 +132,17 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
}
|
||||
|
||||
// Special handling for "On Due" option
|
||||
if (field === 'when' && value === 'ondue') {
|
||||
if (field === 'type' && value === 'ondue') {
|
||||
// Set default values for On Due (not applicable)
|
||||
updatedNotification = {
|
||||
...updatedNotification,
|
||||
amount: 0,
|
||||
unit: 'minutes', // default unit, not displayed to user
|
||||
value: 1,
|
||||
unit: 'minutes',
|
||||
}
|
||||
|
||||
// Check if another notification is already "On Due"
|
||||
const existingOnDue = notifications.findIndex(
|
||||
(n, i) => i !== idx && n.when === 'ondue',
|
||||
(n, i) => i !== idx && n.type === 'ondue',
|
||||
)
|
||||
|
||||
if (existingOnDue !== -1) {
|
||||
@@ -157,7 +153,6 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates when changing a notification
|
||||
if (isDuplicate(updatedNotification, idx)) {
|
||||
setError(
|
||||
'This notification setting already exists. Please use a different timing.',
|
||||
@@ -172,12 +167,9 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleNameChange = e => {
|
||||
setTemplateName(e.target.value)
|
||||
}
|
||||
const addSmartNotification = type => {
|
||||
if (notifications.length >= maxNotifications) return
|
||||
|
||||
setShowSaveDefault(true)
|
||||
let newNotification
|
||||
let suggestions = []
|
||||
|
||||
@@ -185,28 +177,28 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
case 'reminder':
|
||||
// Suggest common reminder times that don't exist
|
||||
suggestions = [
|
||||
{ amount: 1, unit: 'hours', when: 'before' },
|
||||
{ amount: 1, unit: 'days', when: 'before' },
|
||||
{ amount: 30, unit: 'minutes', when: 'before' },
|
||||
{ amount: 2, unit: 'hours', when: 'before' },
|
||||
{ amount: 3, unit: 'days', when: 'before' },
|
||||
{ value: 1, unit: 'hours', type: 'before' },
|
||||
{ value: 1, unit: 'days', type: 'before' },
|
||||
{ value: 30, unit: 'minutes', type: 'before' },
|
||||
{ value: 2, unit: 'hours', type: 'before' },
|
||||
{ value: 3, unit: 'days', type: 'before' },
|
||||
]
|
||||
break
|
||||
|
||||
case 'due':
|
||||
if (notifications.some(n => n.when === 'ondue')) {
|
||||
if (notifications.some(n => n.type === 'ondue')) {
|
||||
setError('Only one "Due Alert" notification is allowed.')
|
||||
return
|
||||
}
|
||||
newNotification = { amount: 0, unit: 'minutes', when: 'ondue' }
|
||||
newNotification = { value: 0, unit: 'minutes', type: 'ondue' }
|
||||
break
|
||||
|
||||
case 'followup':
|
||||
suggestions = [
|
||||
{ amount: 1, unit: 'hours', when: 'after' },
|
||||
{ amount: 1, unit: 'days', when: 'after' },
|
||||
{ amount: 3, unit: 'days', when: 'after' },
|
||||
{ amount: 1, unit: 'weeks', when: 'after' },
|
||||
{ value: 1, unit: 'hours', type: 'after' },
|
||||
{ value: 1, unit: 'days', type: 'after' },
|
||||
{ value: 3, unit: 'days', type: 'after' },
|
||||
{ value: 1, unit: 'weeks', type: 'after' },
|
||||
]
|
||||
break
|
||||
}
|
||||
@@ -226,14 +218,14 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
(a, b) => {
|
||||
// Convert everything to minutes for consistent comparison
|
||||
const getMinutes = notif => {
|
||||
const { amount, unit, when } = notif
|
||||
const { value, unit, type } = notif
|
||||
// On Due is exactly at due date (0 minutes)
|
||||
if (when === 'ondue') return 0
|
||||
if (type === 'ondue') return 0
|
||||
|
||||
let minutes = amount
|
||||
let minutes = value
|
||||
if (unit === 'hours') minutes *= 60
|
||||
if (unit === 'days') minutes *= 24 * 60
|
||||
return when === 'before' ? -minutes : minutes
|
||||
return type === 'before' ? -minutes : minutes
|
||||
}
|
||||
|
||||
return getMinutes(a) - getMinutes(b)
|
||||
@@ -249,20 +241,19 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
setNotifications(updated)
|
||||
onChange && onChange(updated)
|
||||
}
|
||||
// Visualization: improved timeline with better scaling
|
||||
const renderTimeline = () => {
|
||||
// Sort notifications chronologically
|
||||
const sorted = [...notifications].sort((a, b) => {
|
||||
// Convert everything to minutes for consistent comparison
|
||||
const getMinutes = notif => {
|
||||
const { amount, unit, when } = notif
|
||||
const { value, unit, type } = notif
|
||||
// On Due is exactly at due date (0 minutes)
|
||||
if (when === 'ondue') return 0
|
||||
if (type === 'ondue') return 0
|
||||
|
||||
let minutes = amount
|
||||
let minutes = value
|
||||
if (unit === 'hours') minutes *= 60
|
||||
if (unit === 'days') minutes *= 24 * 60
|
||||
return when === 'before' ? -minutes : minutes
|
||||
return type === 'before' ? -minutes : minutes
|
||||
}
|
||||
|
||||
return getMinutes(a) - getMinutes(b)
|
||||
@@ -274,27 +265,24 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
// Find the original index of this item in notifications array
|
||||
const originalIdx = notifications.findIndex(
|
||||
n =>
|
||||
n.amount === item.amount &&
|
||||
n.value === item.value &&
|
||||
n.unit === item.unit &&
|
||||
n.when === item.when,
|
||||
n.type === item.type,
|
||||
)
|
||||
notificationIndexMap[originalIdx] = index + 1
|
||||
}) // Get min and max notification times for dynamic scaling
|
||||
const minutesValues = sorted.map(n => {
|
||||
// On Due is exactly at due date (0 minutes)
|
||||
if (n.when === 'ondue') return 0
|
||||
if (n.type === 'ondue') return 0
|
||||
|
||||
let minutes = n.amount
|
||||
let minutes = n.value
|
||||
if (n.unit === 'hours') minutes *= 60
|
||||
if (n.unit === 'days') minutes *= 24 * 60
|
||||
return n.when === 'before' ? -minutes : minutes
|
||||
return n.type === 'before' ? -minutes : minutes
|
||||
})
|
||||
|
||||
// Find min (before) and max (after) notification times
|
||||
const minBefore = Math.min(0, ...minutesValues) // Default to 0 if no "before" notifications
|
||||
const maxAfter = Math.max(0, ...minutesValues) // Default to 0 if no "after" notifications
|
||||
|
||||
// Dynamic scaling based on notification range
|
||||
const getPositionPercent = minutes => {
|
||||
// Due date is always at center (50%)
|
||||
if (minutes === 0) return 50
|
||||
@@ -314,7 +302,7 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<Typography level={'body2'} sx={{ mb: 1, fontWeight: 'md' }}>
|
||||
<Typography level={'body-md'} sx={{ mb: 1 }}>
|
||||
Notification Timeline
|
||||
</Typography>
|
||||
<Box
|
||||
@@ -322,7 +310,7 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
height: 100,
|
||||
height: 90,
|
||||
bgcolor: 'background.level1',
|
||||
borderRadius: 'md',
|
||||
p: 2,
|
||||
@@ -375,11 +363,11 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
{sorted.map((n, i) => {
|
||||
// Convert to minutes for consistent scale
|
||||
let minutes = 0
|
||||
if (n.when !== 'ondue') {
|
||||
minutes = n.amount
|
||||
if (n.type !== 'ondue') {
|
||||
minutes = n.value
|
||||
if (n.unit === 'hours') minutes *= 60
|
||||
if (n.unit === 'days') minutes *= 24 * 60
|
||||
if (n.when === 'before') minutes = -minutes
|
||||
if (n.type === 'before') minutes = -minutes
|
||||
}
|
||||
// On Due notifications are always at the due date (0 minutes)
|
||||
|
||||
@@ -394,9 +382,9 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
left: `${percent}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
color:
|
||||
n.when === 'before'
|
||||
n.type === 'before'
|
||||
? 'primary.600'
|
||||
: n.when === 'ondue'
|
||||
: n.type === 'ondue'
|
||||
? 'warning.600'
|
||||
: 'success.600',
|
||||
display: 'flex',
|
||||
@@ -418,11 +406,11 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
size={'sm'}
|
||||
variant={'solid'}
|
||||
color={
|
||||
n.when === 'before'
|
||||
? 'primary'
|
||||
: n.when === 'ondue'
|
||||
n.type === 'before'
|
||||
? 'success'
|
||||
: n.type === 'ondue'
|
||||
? 'warning'
|
||||
: 'success'
|
||||
: 'danger'
|
||||
}
|
||||
sx={{
|
||||
'--Badge-paddingX': '4px',
|
||||
@@ -452,22 +440,24 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
borderRadius: 2,
|
||||
p: 3,
|
||||
maxWidth: 500,
|
||||
bgcolor: 'background.body',
|
||||
boxShadow: 'sm',
|
||||
}}
|
||||
sx={
|
||||
{
|
||||
// border: '1px solid',
|
||||
// borderColor: 'neutral.outlinedBorder',
|
||||
// borderRadius: 2,
|
||||
// p: 3,
|
||||
// maxWidth: 500,
|
||||
// bgcolor: 'background.body',
|
||||
// boxShadow: 'sm',
|
||||
}
|
||||
}
|
||||
>
|
||||
<Typography level={'h4'} sx={{ mb: 2 }}>
|
||||
{/* <Typography level={'h4'} sx={{ mb: 2 }}>
|
||||
Schedule Name
|
||||
</Typography>
|
||||
</Typography> */}
|
||||
|
||||
{/* Template Name Field */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{/* <Box sx={{ mb: 3 }}>
|
||||
<Typography level={'body2'} sx={{ mb: 1, fontWeight: 'md' }}>
|
||||
Template Name
|
||||
</Typography>
|
||||
@@ -477,7 +467,7 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
placeholder='Enter template name'
|
||||
sx={{ width: '100%' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box> */}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
@@ -500,9 +490,9 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 30,
|
||||
mr: 1,
|
||||
justifyContent: 'start',
|
||||
width: 18,
|
||||
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
@@ -512,23 +502,23 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
sx={{
|
||||
'--Badge-minHeight': '20px',
|
||||
'--Badge-fontSize': '0.75rem',
|
||||
// centering the badge:
|
||||
}}
|
||||
color={
|
||||
n.when === 'before'
|
||||
? 'primary'
|
||||
: n.when === 'ondue'
|
||||
n.type === 'before'
|
||||
? 'success'
|
||||
: n.type === 'ondue'
|
||||
? 'warning'
|
||||
: 'success'
|
||||
: 'danger'
|
||||
}
|
||||
>
|
||||
<Box sx={{ width: 4, height: 16 }} />
|
||||
{/* Empty box to attach badge to */}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Select
|
||||
value={n.when}
|
||||
onChange={(_, value) => handleChange(idx, 'when', value)}
|
||||
sx={{ mr: 1, minWidth: 120 }}
|
||||
value={n.type}
|
||||
onChange={(_, value) => handleChange(idx, 'type', value)}
|
||||
sx={{ mr: 1, minWidth: 100 }}
|
||||
size={'sm'}
|
||||
>
|
||||
{beforeAfterOptions.map(opt => (
|
||||
@@ -541,32 +531,32 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
<Input
|
||||
type={'number'}
|
||||
min={1}
|
||||
disabled={n.when === 'ondue'}
|
||||
value={n.when === 'ondue' ? '—' : n.amount}
|
||||
disabled={n.type === 'ondue'}
|
||||
value={n.type === 'ondue' ? '—' : n.value}
|
||||
onChange={e =>
|
||||
handleChange(idx, 'amount', Math.max(1, Number(e.target.value)))
|
||||
handleChange(idx, 'value', Math.max(1, Number(e.target.value)))
|
||||
}
|
||||
sx={{
|
||||
width: 70,
|
||||
mr: 1,
|
||||
opacity: n.when === 'ondue' ? 0.6 : 1,
|
||||
...(n.when === 'ondue' && {
|
||||
opacity: n.type === 'ondue' ? 0.6 : 1,
|
||||
...(n.type === 'ondue' && {
|
||||
'& input': {
|
||||
textAlign: 'center',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
size={'sm'}
|
||||
placeholder={n.when === 'ondue' ? '—' : ''}
|
||||
placeholder={n.type === 'ondue' ? '—' : ''}
|
||||
/>
|
||||
<Select
|
||||
value={n.unit}
|
||||
disabled={n.when === 'ondue'}
|
||||
disabled={n.type === 'ondue'}
|
||||
onChange={(_, value) => handleChange(idx, 'unit', value)}
|
||||
sx={{
|
||||
mr: 1,
|
||||
minWidth: 100,
|
||||
opacity: n.when === 'ondue' ? 0.6 : 1,
|
||||
minWidth: 80,
|
||||
opacity: n.type === 'ondue' ? 0.6 : 1,
|
||||
}}
|
||||
size={'sm'}
|
||||
>
|
||||
@@ -604,7 +594,7 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
onClick={() => addSmartNotification('due')}
|
||||
disabled={
|
||||
notifications.length >= maxNotifications ||
|
||||
notifications.some(n => n.when === 'ondue')
|
||||
notifications.some(n => n.type === 'ondue')
|
||||
}
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
@@ -619,12 +609,31 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
variant={'outlined'}
|
||||
color={'success'}
|
||||
color={'danger'}
|
||||
>
|
||||
Follow-up
|
||||
</Button>
|
||||
</Box>
|
||||
{renderTimeline()}
|
||||
{showSaveDefault && (
|
||||
<Button
|
||||
variant='outlined'
|
||||
size='sm'
|
||||
color='neutral'
|
||||
// sx={{ ml: 'auto', mt: 0.5 }}
|
||||
startDecorator={<Save />}
|
||||
onClick={() => {
|
||||
localStorage.setItem(
|
||||
'defaultNotificationTemplate',
|
||||
JSON.stringify(notifications),
|
||||
)
|
||||
setShowSaveDefault(false)
|
||||
}}
|
||||
>
|
||||
Save as Default for Future Tasks
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showTimeline && renderTimeline()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user