Refactor error handling and notification system

- Removed the ErrorProvider component and replaced it with a unified NotificationProvider.
- Updated all components to utilize the new NotificationProvider for displaying notifications.
- Enhanced notification types to include success, error, warning, and info with customizable messages and titles.
- Removed Snackbar components from various views and replaced them with calls to the NotificationProvider.
- Updated the ForgotPasswordView, LoginView, SignupView, and other components to handle notifications more effectively.
- Improved user experience by providing consistent notification handling across the application.
This commit is contained in:
Mo Tarbin
2025-06-13 23:52:02 -04:00
parent bc2ca86a95
commit 2077b0ab4c
19 changed files with 540 additions and 387 deletions

View File

@@ -1,15 +1,18 @@
import NavBar from '@/views/components/NavBar'
import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy'
import { Button, Typography, useColorScheme } from '@mui/joy'
import Tracker from '@openreplay/tracker'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { useRegisterSW } from 'virtual:pwa-register/react'
import { registerCapacitorListeners } from './CapacitorListener'
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
import { useResource } from './queries/ResourceQueries'
import { AuthenticationProvider } from './service/AuthenticationService'
import { ErrorProvider } from './service/ErrorProvider'
import {
NotificationProvider,
useNotification,
} from './service/NotificationProvider'
import { apiManager } from './utils/TokenManager'
import NetworkBanner from './views/components/NetworkBanner'
const add = className => {
@@ -22,22 +25,15 @@ const remove = className => {
// TODO: Update the interval to at 60 minutes
const intervalMS = 5 * 60 * 1000 // 5 minutes
const queryClient = new QueryClient({})
function App() {
const resource = useResource()
const navigate = useNavigate()
startApiManager(navigate)
startOpenReplay()
const { mode, systemMode } = useColorScheme()
const [showUpdateSnackbar, setShowUpdateSnackbar] = useState(true)
const AppContent = () => {
const { showNotification } = useNotification()
const {
offlineReady: [offlineReady, setOfflineReady],
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegistered(r) {
// eslint-disable-next-line prefer-template
console.log('SW Registered: ' + r)
r &&
setInterval(() => {
@@ -48,10 +44,53 @@ function App() {
console.log('SW registration error', error)
},
})
const close = () => {
setOfflineReady(false)
setNeedRefresh(false)
}
useEffect(() => {
if (needRefresh) {
showNotification({
type: 'custom',
component: (
<div>
<Typography level='body-md'>
A new version is now available. Click on reload button to update.
</Typography>
<Button
color='secondary'
size='small'
onClick={() => {
updateServiceWorker(true)
setNeedRefresh(false)
}}
sx={{ ml: 2 }}
>
Refresh
</Button>
</div>
),
snackbarProps: {
autoHideDuration: null, // Persistent until user action
},
})
}
}, [needRefresh, showNotification, updateServiceWorker, setNeedRefresh])
return (
<>
<ImpersonateUserProvider>
<NavBar />
<Outlet />
</ImpersonateUserProvider>
</>
)
}
function App() {
const resource = useResource()
const navigate = useNavigate()
startApiManager(navigate)
startOpenReplay()
const { mode, systemMode } = useColorScheme()
const setThemeClass = () => {
const value = JSON.parse(localStorage.getItem('themeMode')) || mode
@@ -73,6 +112,7 @@ function App() {
useEffect(() => {
setThemeClass()
}, [mode, systemMode])
useEffect(() => {
registerCapacitorListeners()
}, [])
@@ -83,30 +123,9 @@ function App() {
<QueryClientProvider client={queryClient}>
<AuthenticationProvider />
<ErrorProvider>
<ImpersonateUserProvider>
<NavBar />
<Outlet />
</ImpersonateUserProvider>
</ErrorProvider>
{needRefresh && (
<Snackbar open={showUpdateSnackbar}>
<Typography level='body-md'>
A new version is now available.Click on reload button to update.
</Typography>
<Button
color='secondary'
size='small'
onClick={() => {
updateServiceWorker(true)
setShowUpdateSnackbar(false)
}}
>
Refresh
</Button>
</Snackbar>
)}
<NotificationProvider>
<AppContent />
</NotificationProvider>
</QueryClientProvider>
</div>
)

View File

@@ -1,51 +0,0 @@
import { Error } from '@mui/icons-material'
import { Box, Button, Snackbar, Typography } from '@mui/joy'
import React, { createContext, useContext, useState } from 'react'
const ErrorContext = createContext()
export const useError = () => useContext(ErrorContext)
export const ErrorProvider = ({ children }) => {
const [error, setError] = useState(null)
const showError = error => {
setError(error)
}
return (
<ErrorContext.Provider value={{ showError }}>
{children}
<Snackbar
open={Boolean(error)}
autoHideDuration={6000}
onClose={() => setError(null)}
startDecorator={<Error color='danger' />}
endDecorator={
<Button
variant='outlined'
color='danger'
onClick={() => setError(null)}
>
Dismiss
</Button>
}
>
{typeof error === 'string' ? (
<Typography color='danger' level='body-md'>
{error}
</Typography>
) : (
<Box>
<Typography color='danger' level='title-sm'>
{error?.title}
</Typography>
<Typography color='danger' level='body-sm'>
{error?.message}
</Typography>
</Box>
)}
</Snackbar>
</ErrorContext.Provider>
)
}

View File

@@ -0,0 +1,246 @@
import { CheckCircle, Error, Info, Warning } from '@mui/icons-material'
import { Box, Button, Snackbar, Typography } from '@mui/joy'
import React, { createContext, useContext, useState } from 'react'
const NotificationContext = createContext()
export const useNotification = () => useContext(NotificationContext)
// For backward compatibility
export const useError = () => {
const { showError } = useNotification()
return { showError }
}
// Notification types configuration with default titles
const NOTIFICATION_TYPES = {
error: {
color: 'danger',
icon: <Error color='danger' />,
autoHideDuration: 6000,
showDismissButton: true,
defaultTitle: 'Error',
},
success: {
color: 'success',
icon: <CheckCircle color='success' />,
autoHideDuration: 3000,
showDismissButton: false,
defaultTitle: 'Success',
},
warning: {
color: 'warning',
icon: <Warning color='warning' />,
autoHideDuration: 4000,
showDismissButton: false,
defaultTitle: 'Warning',
},
info: {
color: 'primary',
icon: <Info color='primary' />,
autoHideDuration: 4000,
showDismissButton: false,
defaultTitle: 'Information',
},
custom: {
color: 'neutral',
icon: null,
autoHideDuration: null,
showDismissButton: false,
defaultTitle: 'Notification',
},
}
export const NotificationProvider = ({ children }) => {
const [notifications, setNotifications] = useState([])
const addNotification = notification => {
const id = Date.now() + Math.random()
const newNotification = {
id,
...notification,
timestamp: Date.now(),
}
setNotifications(prev => [...prev, newNotification])
// Auto-remove notification if it has a duration
const config =
NOTIFICATION_TYPES[notification.type] || NOTIFICATION_TYPES.info
if (config.autoHideDuration) {
setTimeout(() => {
removeNotification(id)
}, config.autoHideDuration)
}
return id
}
const removeNotification = id => {
setNotifications(prev => prev.filter(n => n.id !== id))
}
const clearAllNotifications = () => {
setNotifications([])
}
// Helper function to normalize notification input
const normalizeNotification = (input, type) => {
if (typeof input === 'string') {
return {
type,
message: input,
}
}
if (typeof input === 'object' && input !== null) {
// If it's already a properly structured notification
if (input.title || input.message) {
return {
type,
...input,
}
}
// If it's a simple object with just message content
return {
type,
message: input.message || input.toString(),
title: input.title,
...input,
}
}
return {
type,
message: input?.toString() || 'Unknown notification',
}
}
// Unified notification method
const showNotification = notification => {
// Handle different input formats
if (typeof notification === 'string') {
return addNotification(normalizeNotification(notification, 'info'))
}
return addNotification(
normalizeNotification(notification, notification.type || 'info'),
)
}
// Specific notification methods with enhanced language
const showError = error => {
return addNotification(normalizeNotification(error, 'error'))
}
const showSuccess = message => {
return addNotification(normalizeNotification(message, 'success'))
}
const showWarning = message => {
return addNotification(normalizeNotification(message, 'warning'))
}
const showInfo = message => {
return addNotification(normalizeNotification(message, 'info'))
}
const renderNotification = notification => {
const config =
NOTIFICATION_TYPES[notification.type] || NOTIFICATION_TYPES.info
// Handle custom notifications with components
if (notification.type === 'custom' && notification.component) {
return (
<Snackbar
key={notification.id}
open={true}
onClose={() => removeNotification(notification.id)}
anchorOrigin={
notification.anchorOrigin || {
vertical: 'bottom',
horizontal: 'right',
}
}
{...(notification.snackbarProps || {})}
>
{React.cloneElement(notification.component, {
onClose: () => removeNotification(notification.id),
...notification.componentProps,
})}
</Snackbar>
)
}
// Handle standard notifications
// Determine the icon to use
const notificationIcon = notification.icon || config.icon
// Determine title and message
const title = notification.title || config.defaultTitle
const message = notification.message
return (
<Snackbar
key={notification.id}
open={true}
autoHideDuration={config.autoHideDuration}
onClose={() => removeNotification(notification.id)}
startDecorator={notificationIcon}
endDecorator={
config.showDismissButton ? (
<Button
variant='outlined'
color={config.color}
onClick={() => removeNotification(notification.id)}
>
Dismiss
</Button>
) : null
}
anchorOrigin={
notification.anchorOrigin || {
vertical: 'bottom',
horizontal: 'right',
}
}
{...(notification.snackbarProps || {})}
>
{/* Enhanced structure like ErrorProvider - always show title and message for consistency */}
{title && message ? (
<Box>
<Typography color={config.color} level='title-sm'>
{title}
</Typography>
<Typography color={config.color} level='body-sm'>
{message}
</Typography>
</Box>
) : (
<Typography color={config.color} level='body-md'>
{message || title || 'Notification'}
</Typography>
)}
</Snackbar>
)
}
return (
<NotificationContext.Provider
value={{
showNotification,
showError,
showSuccess,
showWarning,
showInfo,
removeNotification,
clearAllNotifications,
notifications,
}}
>
{children}
{notifications.map(renderNotification)}
</NotificationContext.Provider>
)
}

View File

@@ -8,21 +8,19 @@ import {
FormHelperText,
Input,
Sheet,
Snackbar,
Typography,
} from '@mui/joy'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from './../../Config'
import { ResetPassword } from '../../utils/Fetcher'
import { useNotification } from '../../service/NotificationProvider'
import { ResetPassword } from '../../utils/Fetcher'
const ForgotPasswordView = () => {
const navigate = useNavigate()
// const [showLoginSnackbar, setShowLoginSnackbar] = useState(false)
// const [snackbarMessage, setSnackbarMessage] = useState('')
const [resetStatusOk, setResetStatusOk] = useState(null)
const [email, setEmail] = useState('')
const [emailError, setEmailError] = useState(null)
const { showError, showNotification } = useNotification()
const validateEmail = email => {
return !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)
@@ -48,12 +46,24 @@ const ForgotPasswordView = () => {
if (response.ok) {
setResetStatusOk(true)
// wait 3 seconds and then redirect to login:
showNotification({
type: 'success',
title: 'Reset Email Sent',
message: 'Check your email for password reset instructions',
})
} else {
setResetStatusOk(false)
showError({
title: 'Reset Failed',
message: 'Failed to send reset email, please try again later',
})
}
} catch (error) {
setResetStatusOk(false)
showError({
title: 'Reset Failed',
message: 'Failed to send reset email, please try again later',
})
}
}
@@ -195,19 +205,6 @@ const ForgotPasswordView = () => {
</Button>
</>
)}
<Snackbar
open={resetStatusOk ? resetStatusOk : resetStatusOk === false}
autoHideDuration={5000}
onClose={() => {
if (resetStatusOk) {
navigate('/login')
}
}}
>
{resetStatusOk
? 'Reset email sent, check your email'
: 'Reset email failed, try again later'}
</Snackbar>
</Sheet>
</Box>
</Container>

View File

@@ -1,23 +1,15 @@
import { Preferences } from '@capacitor/preferences'
import {
Box,
Button,
Container,
Input,
Sheet,
Snackbar,
Typography,
} from '@mui/joy'
import { Box, Button, Container, Input, Sheet, Typography } from '@mui/joy'
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from '../../Config'
import Logo from '../../Logo'
import { useNotification } from '../../service/NotificationProvider'
import { apiManager } from '../../utils/TokenManager'
const LoginSettings = () => {
const [error, setError] = React.useState(null)
const Navigate = useNavigate()
const [serverURL, setServerURL] = React.useState('')
const { showError } = useNotification()
React.useEffect(() => {
Preferences.get({ key: 'customServerUrl' }).then(result => {
@@ -112,7 +104,10 @@ const LoginSettings = () => {
return
}
if (!isValidServerURL()) {
setError('Invalid server URL')
showError({
title: 'Invalid Server URL',
message: 'Please enter a valid server URL with protocol (http:// or https://)',
})
return
}
Preferences.set({
@@ -150,14 +145,6 @@ const LoginSettings = () => {
</Button>
</Sheet>
</Box>
<Snackbar
open={error !== null}
onClose={() => setError(null)}
autoHideDuration={3000}
message={error}
>
{error}
</Snackbar>
</Container>
)
}

View File

@@ -12,7 +12,6 @@ import {
IconButton,
Input,
Sheet,
Snackbar,
Typography,
} from '@mui/joy'
import Cookies from 'js-cookie'
@@ -23,6 +22,7 @@ import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
import Logo from '../../Logo'
import { useResource } from '../../queries/ResourceQueries'
import { useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import { login } from '../../utils/Fetcher'
import { apiManager } from '../../utils/TokenManager'
import MFAVerificationModal from './MFAVerificationModal'
@@ -33,10 +33,10 @@ const LoginView = () => {
const [userProfile, setUserProfile] = useState(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const [mfaModalOpen, setMfaModalOpen] = useState(false)
const [mfaSessionToken, setMfaSessionToken] = useState('')
const { data: resource } = useResource()
const { showError } = useNotification()
const Navigate = useNavigate()
useEffect(() => {
const initializeSocialLogin = async () => {
@@ -76,14 +76,23 @@ const LoginView = () => {
}
})
} else if (response.status === 401) {
setError('Wrong username or password')
showError({
title: 'Login Failed',
message: 'Wrong username or password',
})
} else {
setError('An error occurred, please try again')
showError({
title: 'Login Failed',
message: 'An error occurred, please try again',
})
console.log('Login failed')
}
})
.catch(err => {
setError('Unable to communicate with server, please try again')
showError({
title: 'Connection Error',
message: 'Unable to communicate with server, please try again',
})
console.log('Login failed', err)
})
}
@@ -133,7 +142,10 @@ const LoginView = () => {
})
}
return response.json().then(() => {
setError("Couldn't log in with Google, please try again")
showError({
title: 'Google Login Failed',
message: "Couldn't log in with Google, please try again",
})
})
})
}
@@ -167,7 +179,10 @@ const LoginView = () => {
}
const handleMFAError = errorMessage => {
setError(errorMessage)
showError({
title: 'Two-Factor Authentication Failed',
message: errorMessage,
})
}
const handleMFAClose = () => {
@@ -380,7 +395,10 @@ const LoginView = () => {
loggedWithProvider(provider, data)
}}
onReject={() => {
setError("Couldn't log in with Google, please try again")
showError({
title: 'Google Login Failed',
message: "Couldn't log in with Google, please try again",
})
}}
>
<Button
@@ -466,14 +484,6 @@ const LoginView = () => {
</Button>
</Sheet>
</Box>
<Snackbar
open={error !== null}
onClose={() => setError(null)}
autoHideDuration={3000}
message={error}
>
{error}
</Snackbar>
<MFAVerificationModal
open={mfaModalOpen}

View File

@@ -7,12 +7,12 @@ import {
FormHelperText,
Input,
Sheet,
Snackbar,
Typography,
} from '@mui/joy'
import React from 'react'
import { useNavigate } from 'react-router-dom'
import Logo from '../../Logo'
import { useNotification } from '../../service/NotificationProvider'
import { login, signUp } from '../../utils/Fetcher'
const SignupView = () => {
@@ -25,9 +25,7 @@ const SignupView = () => {
const [passwordError, setPasswordError] = React.useState('')
const [emailError, setEmailError] = React.useState('')
const [displayNameError, setDisplayNameError] = React.useState('')
const [error, setError] = React.useState(null)
const [snackbarOpen, setSnackbarOpen] = React.useState(false)
const [snackbarMessage, setSnackbarMessage] = React.useState('')
const { showError } = useNotification()
const handleLogin = (username, password) => {
login(username, password).then(response => {
if (response.status === 200) {
@@ -104,11 +102,17 @@ const SignupView = () => {
if (response.status === 201) {
handleLogin(username, password)
} else if (response.status === 403) {
setError('Signup disabled, please contact admin')
showError({
title: 'Signup Failed',
message: 'Signup disabled, please contact admin'
})
} else {
console.log('Signup failed')
response.json().then(res => {
setError(res.error)
showError({
title: 'Signup Failed',
message: res.error || 'An error occurred during signup'
})
})
}
})
@@ -264,14 +268,6 @@ const SignupView = () => {
</Button>
</Sheet>
</Box>
<Snackbar
open={error !== null}
onClose={() => setError(null)}
autoHideDuration={5000}
message={error}
>
{error}
</Snackbar>
</Container>
)
}

View File

@@ -7,13 +7,13 @@ import {
FormHelperText,
Input,
Sheet,
Snackbar,
Typography,
} from '@mui/joy'
import { useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import Logo from '../../Logo'
import { useNotification } from '../../service/NotificationProvider'
import { ChangePassword } from '../../utils/Fetcher'
const UpdatePasswordView = () => {
@@ -24,8 +24,7 @@ const UpdatePasswordView = () => {
const [passworConfirmationError, setPasswordConfirmationError] =
useState(null)
const [searchParams] = useSearchParams()
const [updateStatusOk, setUpdateStatusOk] = useState(null)
const { showError, showNotification } = useNotification()
const verifiticationCode = searchParams.get('c')
@@ -55,16 +54,27 @@ const UpdatePasswordView = () => {
const response = await ChangePassword(verifiticationCode, password)
if (response.ok) {
setUpdateStatusOk(true)
showNotification({
type: 'success',
title: 'Password Updated',
message:
'Your password has been updated successfully. Redirecting to login...',
})
// wait 3 seconds and then redirect to login:
setTimeout(() => {
navigate('/login')
}, 3000)
} else {
setUpdateStatusOk(false)
showError({
title: 'Password Update Failed',
message: 'Failed to update password, please try again later',
})
}
} catch (error) {
setUpdateStatusOk(false)
showError({
title: 'Password Update Failed',
message: 'Failed to update password, please try again later',
})
}
}
return (
@@ -169,15 +179,6 @@ const UpdatePasswordView = () => {
</Button>
</Sheet>
</Box>
<Snackbar
open={updateStatusOk === false}
autoHideDuration={6000}
onClose={() => {
setUpdateStatusOk(null)
}}
>
Password update failed, try again later
</Snackbar>
</Container>
)
}

View File

@@ -18,7 +18,6 @@ import {
RadioGroup,
Select,
Sheet,
Snackbar,
Stack,
Switch,
Typography,
@@ -33,6 +32,7 @@ import {
useUpdateChore,
} from '../../queries/ChoreQueries.jsx'
import { useUserProfile } from '../../queries/UserQueries.jsx'
import { useNotification } from '../../service/NotificationProvider'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
DeleteChore,
@@ -101,9 +101,6 @@ const ChoreEdit = () => {
const [createdBy, setCreatedBy] = useState(0)
const [errors, setErrors] = useState({})
const [attemptToSave, setAttemptToSave] = useState(false)
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarColor, setSnackbarColor] = useState('warning')
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels()
const updateChoreMutation = useUpdateChore()
@@ -113,6 +110,7 @@ const ChoreEdit = () => {
isLoading: isChoreLoading,
refetch: refetchChore,
} = useChore(choreId)
const { showSuccess, showError } = useNotification()
const [userLabels, setUserLabels] = useState([])
@@ -178,16 +176,10 @@ const ChoreEdit = () => {
const errorList = Object.keys(errors).map(key => (
<ListItem key={key}>{errors[key]}</ListItem>
))
setSnackbarMessage(
<Stack spacing={0.5}>
<Typography level='title-md'>
Please resolve the following errors:
</Typography>
<List>{errorList}</List>
</Stack>,
)
setSnackbarColor('danger')
setIsSnackbarOpen(true)
showError({
title: 'Please resolve the following errors:',
message: <List>{errorList}</List>,
})
return false
}
@@ -240,16 +232,18 @@ const ChoreEdit = () => {
SaveFunction(chore)
.then(() => {
setSnackbarColor('success')
setSnackbarMessage('Chore saved successfully!')
setIsSnackbarOpen(true)
showSuccess({
title: 'Chore Saved',
message: 'Your task has been saved successfully!',
})
Navigate('/my/chores/')
})
.catch(error => {
console.error('Failed to save chore:', error)
setSnackbarColor('danger')
setSnackbarMessage('Failed to save chore, please try again.')
setIsSnackbarOpen(true)
showError({
title: 'Save Failed',
message: 'Failed to save chore, please try again.',
})
})
}
useEffect(() => {
@@ -1099,20 +1093,6 @@ const ChoreEdit = () => {
/>
)}
{/* <ChoreHistory ChoreHistory={choresHistory} UsersData={performers} /> */}
<Snackbar
open={isSnackbarOpen}
onClose={() => {
setIsSnackbarOpen(false)
setSnackbarMessage(null)
}}
color={snackbarColor}
autoHideDuration={4000}
sx={{ bottom: 70 }}
invertedColors={true}
variant='soft'
>
{snackbarMessage}
</Snackbar>
</Container>
)
}

View File

@@ -23,7 +23,7 @@ import React from 'react'
import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useUserProfile } from '../../queries/UserQueries.jsx'
import { useError } from '../../service/ErrorProvider'
import { useNotification } from '../../service/NotificationProvider'
import { notInCompletionWindow } from '../../utils/Chores.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
@@ -67,7 +67,7 @@ const ChoreCard = ({
const { impersonatedUser } = useImpersonateUser()
const { showError } = useError()
const { showError } = useNotification()
const handleDelete = () => {
setConfirmModelConfig({

View File

@@ -19,7 +19,7 @@ import React from 'react'
import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useUserProfile } from '../../queries/UserQueries.jsx'
import { useError } from '../../service/ErrorProvider'
import { useNotification } from '../../service/NotificationProvider'
import { notInCompletionWindow } from '../../utils/Chores.jsx'
import {
getTextColorFromBackgroundColor,
@@ -66,7 +66,7 @@ const CompactChoreCard = ({
const { impersonatedUser } = useImpersonateUser()
const { showError } = useError()
const { showError } = useNotification()
// All the existing handler methods (same as original ChoreCard)
const handleDelete = () => {
@@ -370,19 +370,14 @@ const CompactChoreCard = ({
switch (priority) {
case 1:
return TASK_COLOR.PRIORITY_1
// return '#e53e3e' // Red for high priority
case 2:
return TASK_COLOR.PRIORITY_2
// return '#d69e2e' // Orange/yellow for medium priority
case 3:
return TASK_COLOR.PRIORITY_3
// return '#3182ce' // Blue for low priority
case 4:
return TASK_COLOR.PRIORITY_4
// return 'rgba(49, 130, 206, 0.5)' // Light blue for very low priority
default:
return TASK_COLOR.NO_PRIORITY
// return 'transparent' // No priority
}
}
@@ -416,7 +411,7 @@ const CompactChoreCard = ({
bottom: 0,
width: '3px',
backgroundColor: getPriorityColor(chore.priority),
borderRadius: '2px',
borderRadius: '16px',
},
}}
onClick={() => navigate(`/chores/${chore.id}`)}

View File

@@ -26,13 +26,13 @@ import {
List,
Menu,
MenuItem,
Snackbar,
Typography,
} from '@mui/joy'
import Fuse from 'fuse.js'
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useChores } from '../../queries/ChoreQueries'
import { useNotification } from '../../service/NotificationProvider'
import { GetArchivedChores } from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
import LoadingComponent from '../components/Loading'
@@ -55,8 +55,7 @@ import SortAndGrouping from './SortAndGrouping'
const MyChores = () => {
const { data: userProfile, isLoading: isUserProfileLoading } =
useUserProfile()
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
const [snackBarMessage, setSnackBarMessage] = useState(null)
const { showSuccess } = useNotification()
const [chores, setChores] = useState([])
const [archivedChores, setArchivedChores] = useState(null)
const [filteredChores, setFilteredChores] = useState([])
@@ -290,24 +289,41 @@ const MyChores = () => {
switch (event) {
case 'completed':
setSnackBarMessage('Completed')
showSuccess({
title: 'Task Completed',
message: 'Great job! The task has been marked as completed.',
})
break
case 'skipped':
setSnackBarMessage('Skipped')
showSuccess({
title: 'Task Skipped',
message: 'The task has been moved to the next due date.',
})
break
case 'rescheduled':
setSnackBarMessage('Rescheduled')
showSuccess({
title: 'Task Rescheduled',
message: 'The task due date has been updated successfully.',
})
break
case 'unarchive':
setSnackBarMessage('Unarchive')
showSuccess({
title: 'Task Restored',
message: 'The task has been restored and is now active.',
})
break
case 'archive':
setSnackBarMessage('Archived')
showSuccess({
title: 'Task Archived',
message: 'The task has been archived and hidden from the active list.',
})
break
default:
setSnackBarMessage('Updated')
showSuccess({
title: 'Task Updated',
message: 'Your changes have been saved successfully.',
})
}
setIsSnackbarOpen(true)
}
const handleChoreDeleted = deletedChore => {
@@ -868,19 +884,6 @@ const MyChores = () => {
/>
</IconButton>
</Box>
<Snackbar
open={isSnackbarOpen}
onClose={() => {
setIsSnackbarOpen(false)
}}
autoHideDuration={3000}
variant='soft'
color='success'
size='lg'
invertedColors
>
<Typography level='title-md'>{snackBarMessage}</Typography>
</Snackbar>
<NotificationAccessSnackbar />
{addTaskModalOpen && (
<TaskInput

View File

@@ -1,78 +1,81 @@
import { Capacitor } from '@capacitor/core';
import { Button, Snackbar, Stack, Typography } from '@mui/joy'
import { Preferences } from '@capacitor/preferences';
import { LocalNotifications } from '@capacitor/local-notifications';
import {React, useEffect, useState} from 'react';
import { Capacitor } from '@capacitor/core'
import { LocalNotifications } from '@capacitor/local-notifications'
import { Preferences } from '@capacitor/preferences'
import { Button, Stack, Typography } from '@mui/joy'
import { useEffect, useState } from 'react'
const NotificationAccessSnackbar = () => {
const [open, setOpen] = useState(false)
if (!Capacitor.isNativePlatform()) {
return null
}
const getNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'notificationPreferences' })
return JSON.parse(ret.value)
}
const [open, setOpen] = useState(false);
if (!Capacitor.isNativePlatform()) {
return null;
}
const getNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'notificationPreferences' });
return JSON.parse(ret.value);
};
useEffect(() => {
getNotificationPreferences().then((data) => {
// if optOut is true then don't show the snackbar
if(data?.optOut === true || data?.granted === true) {
return;
}
setOpen(true);
});
}
, []);
return (
useEffect(() => {
getNotificationPreferences().then(data => {
// if optOut is true then don't show the snackbar
if (data?.optOut === true || data?.granted === true) {
return
}
setOpen(true)
})
}, [])
return (
<Snackbar
// autoHideDuration={5000}
variant="solid"
color="primary"
size="lg"
variant='solid'
color='primary'
size='lg'
invertedColors
open={open}
onClose={() => setOpen(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
sx={(theme) => ({
sx={theme => ({
background: `linear-gradient(45deg, ${theme.palette.primary[600]} 30%, ${theme.palette.primary[500]} 90%})`,
maxWidth: 360,
})}
>
<div>
<Typography level="title-lg">Need Notification?</Typography>
<Typography level='title-lg'>Need Notification?</Typography>
<Typography sx={{ mt: 1, mb: 2 }}>
You need to enable permission to receive notifications, do you want to enable it?
You need to enable permission to receive notifications, do you want to
enable it?
</Typography>
<Stack direction="row" spacing={1}>
<Button variant="solid" color="primary" onClick={() => {
const notificationPreferences = { optOut: false };
LocalNotifications.requestPermissions().then((resp) => {
<Stack direction='row' spacing={1}>
<Button
variant='solid'
color='primary'
onClick={() => {
const notificationPreferences = { optOut: false }
LocalNotifications.requestPermissions().then(resp => {
if (resp.display === 'granted') {
notificationPreferences['granted'] = true;
notificationPreferences['granted'] = true
}
})
Preferences.set({ key: 'notificationPreferences', value: JSON.stringify(notificationPreferences) });
setOpen(false);
}}>
Yes
Preferences.set({
key: 'notificationPreferences',
value: JSON.stringify(notificationPreferences),
})
setOpen(false)
}}
>
Yes
</Button>
<Button
variant="outlined"
color="primary"
variant='outlined'
color='primary'
onClick={() => {
const notificationPreferences = { optOut: true };
Preferences.set({ key: 'notificationPreferences', value: JSON.stringify(notificationPreferences) });
setOpen(false);
const notificationPreferences = { optOut: true }
Preferences.set({
key: 'notificationPreferences',
value: JSON.stringify(notificationPreferences),
})
setOpen(false)
}}
>
No, Keep it Disabled
@@ -80,8 +83,7 @@ return (
</Stack>
</div>
</Snackbar>
)
)
}
export default NotificationAccessSnackbar;
export default NotificationAccessSnackbar

View File

@@ -1,39 +1,42 @@
import { Button, Snackbar } from '@mui/joy'
import { Button } from '@mui/joy'
import Cookies from 'js-cookie'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import { useNotification } from '../../service/NotificationProvider'
const CookiePermissionSnackbar = () => {
const { showNotification } = useNotification()
useEffect(() => {
const cookiePermission = Cookies.get('cookies_permission')
if (cookiePermission !== 'true') {
setOpen(true)
showNotification({
type: 'custom',
component: <CookieAcceptComponent />,
snackbarProps: {
autoHideDuration: null,
},
anchorOrigin: { vertical: 'bottom', horizontal: 'center' },
})
}
}, [])
}, [showNotification])
const [open, setOpen] = useState(false)
const handleClose = () => {
return null
}
const CookieAcceptComponent = ({ onClose }) => {
const handleAccept = () => {
Cookies.set('cookies_permission', 'true')
setOpen(false)
onClose?.()
}
return (
<Snackbar
open={open}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
onClose={(event, reason) => {
if (reason === 'clickaway') {
return
}
// Cookies.set('cookies_permission', 'true')
handleClose()
}}
>
<div>
We use cookies to ensure you get the best experience on our website.
<Button variant='soft' onClick={handleClose}>
<Button variant='soft' onClick={handleAccept} sx={{ ml: 2 }}>
Accept
</Button>
</Snackbar>
</div>
)
}

View File

@@ -12,7 +12,7 @@ import {
import { useEffect, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { useError } from '../../../service/ErrorProvider.jsx'
import { useNotification } from '../../../service/NotificationProvider.jsx'
import LABEL_COLORS from '../../../utils/Colors.jsx'
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
import { useLabels } from '../../Labels/LabelQueries'
@@ -23,7 +23,7 @@ function LabelModal({ isOpen, onClose, label }) {
const [error, setError] = useState('')
const { data: userLabels = [] } = useLabels()
const queryClient = useQueryClient()
const { showError } = useError()
const { showError } = useNotification()
// Populate the form fields when editing
useEffect(() => {

View File

@@ -1,7 +1,6 @@
import { Capacitor } from '@capacitor/core'
import { LocalNotifications } from '@capacitor/local-notifications'
import { Preferences } from '@capacitor/preferences'
import { Close } from '@mui/icons-material'
import {
Box,
Button,
@@ -10,24 +9,23 @@ import {
FormControl,
FormHelperText,
FormLabel,
IconButton,
Input,
Option,
Select,
Snackbar,
Switch,
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
import { useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import {
UpdateNotificationTarget,
UpdateUserDetails,
} from '../../utils/Fetcher'
const NotificationSetting = () => {
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
const { showWarning } = useNotification()
const { data: userProfile, refetch: refetchUserProfile } = useUserProfile()
const getNotificationPreferences = async () => {
@@ -147,7 +145,10 @@ const NotificationSetting = () => {
setDeviceNotification(true)
setNotificationPreferences({ granted: true })
} else if (resp.display === 'denied') {
setIsSnackbarOpen(true)
showWarning({
title: 'Notification Permission Denied',
message: 'You have denied notification permissions. You can enable them later in your device settings.',
})
setDeviceNotification(false)
setNotificationPreferences({ granted: false })
}
@@ -251,12 +252,14 @@ const NotificationSetting = () => {
setPushNotification(true)
setPushNotificationPreferences({granted: true})
}
if (resp.receive!== 'granted') {
setIsSnackbarOpen(true)
if (resp.receive !== 'granted') {
showWarning({
title: 'Push Notification Permission Denied',
message: 'Push notifications have been disabled. You can enable them in your device settings if needed.',
})
setPushNotification(false)
setPushNotificationPreferences({granted: false})
console.log("User denied permission", resp)
}
})
}
@@ -440,30 +443,6 @@ const NotificationSetting = () => {
</Button>
</Box>
)}
<Snackbar
open={isSnackbarOpen}
autoHideDuration={8000}
onClose={() => setIsSnackbarOpen(false)}
endDecorator={
<IconButton size='md' onClick={() => setIsSnackbarOpen(false)}>
<Close />
</IconButton>
}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography level='title-md'>Permission Denied</Typography>
<Typography level='body-md'>
You have denied the permission to receive notification on this
device. Please enable it in your device settings
</Typography>
</div>
</Snackbar>
</div>
)
}

View File

@@ -15,12 +15,11 @@ import {
Container,
Grid,
IconButton,
Snackbar,
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useError } from '../../service/ErrorProvider'
import { useNotification } from '../../service/NotificationProvider'
import {
CreateThing,
DeleteThing,
@@ -169,11 +168,7 @@ const ThingsView = () => {
const [isShowEditThingStateModal, setIsShowEditStateModal] = useState(false)
const [createModalThing, setCreateModalThing] = useState(null)
const [confirmModelConfig, setConfirmModelConfig] = useState({})
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarColor, setSnackbarColor] = useState('success')
const { showError } = useError()
const { showError, showNotification } = useNotification()
useEffect(() => {
// fetch things
@@ -204,9 +199,11 @@ const ThingsView = () => {
currentThings.push(data.res)
setThings(currentThings)
}
setSnackbarMessage('Thing saved successfully')
setSnackbarColor('success')
setIsSnackbarOpen(true)
showNotification({
type: 'success',
title: 'Thing Saved',
message: 'Thing saved successfully',
})
})
})
.catch(error => {
@@ -246,11 +243,10 @@ const ThingsView = () => {
currentThings.splice(thingIndex, 1)
setThings(currentThings)
} else if (response.status === 405) {
setSnackbarMessage(
'Unable to delete thing with associated tasks',
)
setSnackbarColor('danger')
setIsSnackbarOpen(true)
showError({
title: 'Unable to Delete Thing',
message: 'Unable to delete thing with associated tasks',
})
}
// if method not allwo show snackbar:
})
@@ -293,8 +289,11 @@ const ThingsView = () => {
)
currentThings[thingIndex] = data.res
setThings(currentThings)
setSnackbarMessage('Thing state updated successfully')
setIsSnackbarOpen(true)
showNotification({
type: 'success',
title: 'Thing Updated',
message: 'Thing state updated successfully',
})
})
})
.catch(error => {
@@ -399,19 +398,6 @@ const ThingsView = () => {
<ConfirmationModal config={confirmModelConfig} />
</Box>
<Snackbar
open={isSnackbarOpen}
onClose={() => {
setIsSnackbarOpen(false)
}}
autoHideDuration={3000}
variant='soft'
color={snackbarColor}
size='lg'
invertedColors
>
<Typography level='title-md'>{snackbarMessage}</Typography>
</Snackbar>
</Container>
)
}

View File

@@ -17,7 +17,7 @@ import {
import { Divider, IconButton, Menu, MenuItem } from '@mui/joy'
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useError } from '../../service/ErrorProvider'
import { useNotification } from '../../service/NotificationProvider'
import {
ArchiveChore,
DeleteChore,
@@ -41,7 +41,7 @@ const ChoreActionMenu = ({
const [anchorEl, setAnchorEl] = React.useState(null)
const menuRef = React.useRef(null)
const navigate = useNavigate()
const { showError } = useError()
const { showError } = useNotification()
useEffect(() => {
const handleMenuOutsideClick = event => {

View File

@@ -2,9 +2,9 @@ import imageCompression from 'browser-image-compression'
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import QuillMarkdown from 'quilljs-markdown'
import { useCallback, useContext, useEffect, useRef } from 'react'
import { UserContext } from '../../contexts/UserContext'
import { useError } from '../../service/ErrorProvider'
import { useCallback, useEffect, useRef } from 'react'
import { useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
import { UploadFile } from '../../utils/TokenManager'
import './RichTextEditor.css'
@@ -18,8 +18,8 @@ const RichTextEditor = ({
entityId,
entityType,
}) => {
const { showError } = useError()
const { userProfile } = useContext(UserContext)
const { showError } = useNotification()
const { data: userProfile } = useUserProfile()
const quillRef = useRef(null)
const editorRef = useRef(null)