diff --git a/src/App.jsx b/src/App.jsx
index cbad980..314097a 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -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: (
+
+
+ A new version is now available. Click on reload button to update.
+
+ {
+ updateServiceWorker(true)
+ setNeedRefresh(false)
+ }}
+ sx={{ ml: 2 }}
+ >
+ Refresh
+
+
+ ),
+ snackbarProps: {
+ autoHideDuration: null, // Persistent until user action
+ },
+ })
+ }
+ }, [needRefresh, showNotification, updateServiceWorker, setNeedRefresh])
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+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() {
-
-
-
-
-
-
-
- {needRefresh && (
-
-
- A new version is now available.Click on reload button to update.
-
- {
- updateServiceWorker(true)
- setShowUpdateSnackbar(false)
- }}
- >
- Refresh
-
-
- )}
+
+
+
)
diff --git a/src/service/ErrorProvider.jsx b/src/service/ErrorProvider.jsx
deleted file mode 100644
index 5cfc120..0000000
--- a/src/service/ErrorProvider.jsx
+++ /dev/null
@@ -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 (
-
- {children}
- setError(null)}
- startDecorator={ }
- endDecorator={
- setError(null)}
- >
- Dismiss
-
- }
- >
- {typeof error === 'string' ? (
-
- {error}
-
- ) : (
-
-
- {error?.title}
-
-
- {error?.message}
-
-
- )}
-
-
- )
-}
diff --git a/src/service/NotificationProvider.jsx b/src/service/NotificationProvider.jsx
new file mode 100644
index 0000000..9d6dc39
--- /dev/null
+++ b/src/service/NotificationProvider.jsx
@@ -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: ,
+ autoHideDuration: 6000,
+ showDismissButton: true,
+ defaultTitle: 'Error',
+ },
+ success: {
+ color: 'success',
+ icon: ,
+ autoHideDuration: 3000,
+ showDismissButton: false,
+ defaultTitle: 'Success',
+ },
+ warning: {
+ color: 'warning',
+ icon: ,
+ autoHideDuration: 4000,
+ showDismissButton: false,
+ defaultTitle: 'Warning',
+ },
+ info: {
+ color: 'primary',
+ icon: ,
+ 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 (
+ removeNotification(notification.id)}
+ anchorOrigin={
+ notification.anchorOrigin || {
+ vertical: 'bottom',
+ horizontal: 'right',
+ }
+ }
+ {...(notification.snackbarProps || {})}
+ >
+ {React.cloneElement(notification.component, {
+ onClose: () => removeNotification(notification.id),
+ ...notification.componentProps,
+ })}
+
+ )
+ }
+
+ // 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 (
+ removeNotification(notification.id)}
+ startDecorator={notificationIcon}
+ endDecorator={
+ config.showDismissButton ? (
+ removeNotification(notification.id)}
+ >
+ Dismiss
+
+ ) : null
+ }
+ anchorOrigin={
+ notification.anchorOrigin || {
+ vertical: 'bottom',
+ horizontal: 'right',
+ }
+ }
+ {...(notification.snackbarProps || {})}
+ >
+ {/* Enhanced structure like ErrorProvider - always show title and message for consistency */}
+ {title && message ? (
+
+
+ {title}
+
+
+ {message}
+
+
+ ) : (
+
+ {message || title || 'Notification'}
+
+ )}
+
+ )
+ }
+
+ return (
+
+ {children}
+ {notifications.map(renderNotification)}
+
+ )
+}
diff --git a/src/views/Authorization/ForgotPasswordView.jsx b/src/views/Authorization/ForgotPasswordView.jsx
index b8eb6b0..904f14b 100644
--- a/src/views/Authorization/ForgotPasswordView.jsx
+++ b/src/views/Authorization/ForgotPasswordView.jsx
@@ -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 = () => {
>
)}
- {
- if (resetStatusOk) {
- navigate('/login')
- }
- }}
- >
- {resetStatusOk
- ? 'Reset email sent, check your email'
- : 'Reset email failed, try again later'}
-
diff --git a/src/views/Authorization/LoginSettings.jsx b/src/views/Authorization/LoginSettings.jsx
index 22cde4c..7962daa 100644
--- a/src/views/Authorization/LoginSettings.jsx
+++ b/src/views/Authorization/LoginSettings.jsx
@@ -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 = () => {
- setError(null)}
- autoHideDuration={3000}
- message={error}
- >
- {error}
-
)
}
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index 11261ef..aefd3b1 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -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",
+ })
}}
>
{
- setError(null)}
- autoHideDuration={3000}
- message={error}
- >
- {error}
-
{
@@ -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 = () => {
- setError(null)}
- autoHideDuration={5000}
- message={error}
- >
- {error}
-
)
}
diff --git a/src/views/Authorization/UpdatePasswordView.jsx b/src/views/Authorization/UpdatePasswordView.jsx
index 8d82537..d891706 100644
--- a/src/views/Authorization/UpdatePasswordView.jsx
+++ b/src/views/Authorization/UpdatePasswordView.jsx
@@ -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 = () => {
- {
- setUpdateStatusOk(null)
- }}
- >
- Password update failed, try again later
-
)
}
diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx
index e2c8bc0..7a8fd65 100644
--- a/src/views/ChoreEdit/ChoreEdit.jsx
+++ b/src/views/ChoreEdit/ChoreEdit.jsx
@@ -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 => (
{errors[key]}
))
- setSnackbarMessage(
-
-
- Please resolve the following errors:
-
- {errorList}
- ,
- )
- setSnackbarColor('danger')
- setIsSnackbarOpen(true)
+ showError({
+ title: 'Please resolve the following errors:',
+ message: {errorList}
,
+ })
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 = () => {
/>
)}
{/* */}
- {
- setIsSnackbarOpen(false)
- setSnackbarMessage(null)
- }}
- color={snackbarColor}
- autoHideDuration={4000}
- sx={{ bottom: 70 }}
- invertedColors={true}
- variant='soft'
- >
- {snackbarMessage}
-
)
}
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
index 2f806ee..0a1753d 100644
--- a/src/views/Chores/ChoreCard.jsx
+++ b/src/views/Chores/ChoreCard.jsx
@@ -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({
diff --git a/src/views/Chores/CompactChoreCard.jsx b/src/views/Chores/CompactChoreCard.jsx
index 8f101b6..750c643 100644
--- a/src/views/Chores/CompactChoreCard.jsx
+++ b/src/views/Chores/CompactChoreCard.jsx
@@ -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}`)}
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index 68c5784..914406a 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -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 = () => {
/>
- {
- setIsSnackbarOpen(false)
- }}
- autoHideDuration={3000}
- variant='soft'
- color='success'
- size='lg'
- invertedColors
- >
- {snackBarMessage}
-
{addTaskModalOpen && (
{
-
+ 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 (
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,
})}
>
- Need Notification?
+ Need Notification?
- 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?
-
- {
- const notificationPreferences = { optOut: false };
- LocalNotifications.requestPermissions().then((resp) => {
+
+ {
+ 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
{
- 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 (
-
-)
+ )
}
-export default NotificationAccessSnackbar;
\ No newline at end of file
+export default NotificationAccessSnackbar
diff --git a/src/views/Landing/CookiePermissionSnackbar.jsx b/src/views/Landing/CookiePermissionSnackbar.jsx
index 7fe3724..f72b922 100644
--- a/src/views/Landing/CookiePermissionSnackbar.jsx
+++ b/src/views/Landing/CookiePermissionSnackbar.jsx
@@ -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: ,
+ 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 (
- {
- if (reason === 'clickaway') {
- return
- }
- // Cookies.set('cookies_permission', 'true')
- handleClose()
- }}
- >
+
We use cookies to ensure you get the best experience on our website.
-
+
Accept
-
+
)
}
diff --git a/src/views/Modals/Inputs/LabelModal.jsx b/src/views/Modals/Inputs/LabelModal.jsx
index 81118c4..9430209 100644
--- a/src/views/Modals/Inputs/LabelModal.jsx
+++ b/src/views/Modals/Inputs/LabelModal.jsx
@@ -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(() => {
diff --git a/src/views/Settings/NotificationSetting.jsx b/src/views/Settings/NotificationSetting.jsx
index 6e4d8fd..7ec0255 100644
--- a/src/views/Settings/NotificationSetting.jsx
+++ b/src/views/Settings/NotificationSetting.jsx
@@ -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 = () => {
)}
- setIsSnackbarOpen(false)}
- endDecorator={
- setIsSnackbarOpen(false)}>
-
-
- }
- >
-
- Permission Denied
-
- You have denied the permission to receive notification on this
- device. Please enable it in your device settings
-
-
-
)
}
diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx
index dbf3400..17e170a 100644
--- a/src/views/Things/ThingsView.jsx
+++ b/src/views/Things/ThingsView.jsx
@@ -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 = () => {
- {
- setIsSnackbarOpen(false)
- }}
- autoHideDuration={3000}
- variant='soft'
- color={snackbarColor}
- size='lg'
- invertedColors
- >
- {snackbarMessage}
-
)
}
diff --git a/src/views/components/ChoreActionMenu.jsx b/src/views/components/ChoreActionMenu.jsx
index aecf099..e95f349 100644
--- a/src/views/components/ChoreActionMenu.jsx
+++ b/src/views/components/ChoreActionMenu.jsx
@@ -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 => {
diff --git a/src/views/components/RichTextEditor.jsx b/src/views/components/RichTextEditor.jsx
index 3a0eb40..4365ed6 100644
--- a/src/views/components/RichTextEditor.jsx
+++ b/src/views/components/RichTextEditor.jsx
@@ -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)