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:
99
src/App.jsx
99
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: (
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
246
src/service/NotificationProvider.jsx
Normal file
246
src/service/NotificationProvider.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}`)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user