From 2077b0ab4c5c80be94f2b5a850fabedc44dc3370 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Fri, 13 Jun 2025 23:52:02 -0400 Subject: [PATCH] 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. --- src/App.jsx | 99 ++++--- src/service/ErrorProvider.jsx | 51 ---- src/service/NotificationProvider.jsx | 246 ++++++++++++++++++ .../Authorization/ForgotPasswordView.jsx | 35 ++- src/views/Authorization/LoginSettings.jsx | 27 +- src/views/Authorization/LoginView.jsx | 42 +-- src/views/Authorization/Signup.jsx | 24 +- .../Authorization/UpdatePasswordView.jsx | 31 +-- src/views/ChoreEdit/ChoreEdit.jsx | 48 +--- src/views/Chores/ChoreCard.jsx | 4 +- src/views/Chores/CompactChoreCard.jsx | 11 +- src/views/Chores/MyChores.jsx | 49 ++-- .../Chores/NotificationAccessSnackbar.jsx | 110 ++++---- .../Landing/CookiePermissionSnackbar.jsx | 43 +-- src/views/Modals/Inputs/LabelModal.jsx | 4 +- src/views/Settings/NotificationSetting.jsx | 43 +-- src/views/Things/ThingsView.jsx | 46 ++-- src/views/components/ChoreActionMenu.jsx | 4 +- src/views/components/RichTextEditor.jsx | 10 +- 19 files changed, 540 insertions(+), 387 deletions(-) delete mode 100644 src/service/ErrorProvider.jsx create mode 100644 src/service/NotificationProvider.jsx 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. + + +
+ ), + 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. - - - - )} + + + ) 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={ - - } - > - {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 ? ( + + ) : 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? - -
- -) + ) } -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. - - +
) } 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)