Add confirmation modals for token removal and storage clearing actions

This commit is contained in:
Mo Tarbin
2025-06-19 12:40:33 -04:00
parent d8b5e0f5a1
commit c903192d7c
5 changed files with 274 additions and 101 deletions

View File

@@ -50,6 +50,7 @@
"chrono-node": "^2.7.7",
"dotenv": "^16.4.5",
"esm": "^3.2.25",
"event-source-polyfill": "^1.0.31",
"farmhash": "^4.0.1",
"fuse.js": "^7.0.0",
"js-cookie": "^3.0.5",

View File

@@ -63,15 +63,15 @@ const RealTimeSettings = () => {
const getStatusDescription = () => {
if (!isPlusAccount(userProfile)) {
return 'Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when chores are updated.'
return 'Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when tasks are updated.'
}
if (realtimeType === REALTIME_TYPES.DISABLED) {
return 'Real-time updates are disabled. Enable them to see live changes when you or other circle members complete, skip, or modify chores.'
return 'Real-time updates are disabled. Enable them to see live changes when you or other circle members complete, skip, or modify tasks.'
}
if (context.isConnected) {
return "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify chores."
return "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify tasks."
}
if (context.isConnecting) {
@@ -97,12 +97,18 @@ const RealTimeSettings = () => {
return (
<Card sx={{ mt: 2, p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2, mb: 2 }}>
{realtimeType !== REALTIME_TYPES.DISABLED &&
isPlusAccount(userProfile) ? (
<Sync color={context.isConnected ? 'success' : 'disabled'} />
) : (
<SyncDisabled color='disabled' />
)}
<Switch
color='success'
checked={realtimeType !== REALTIME_TYPES.DISABLED}
onChange={e => {
handleRealtimeTypeChange(
null,
e.target.checked ? REALTIME_TYPES.SSE : REALTIME_TYPES.DISABLED,
)
}}
disabled={!isPlusAccount(userProfile)}
inputProps={{ 'aria-label': 'Enable Real-time Updates' }}
/>
<Box sx={{ flex: 1 }}>
<Box
sx={{
@@ -121,22 +127,15 @@ const RealTimeSettings = () => {
)}
</Typography>
<Switch
checked={realtimeType !== REALTIME_TYPES.DISABLED}
onChange={e => {
handleRealtimeTypeChange(
null,
e.target.checked
? REALTIME_TYPES.SSE
: REALTIME_TYPES.DISABLED,
)
}}
disabled={!isPlusAccount(userProfile)}
inputProps={{ 'aria-label': 'Enable Real-time Updates' }}
/>
{realtimeType !== REALTIME_TYPES.DISABLED &&
isPlusAccount(userProfile) ? (
<Sync color={context.isConnected ? 'success' : 'disabled'} />
) : (
<SyncDisabled color='disabled' />
)}
</Box>
<Typography level='body-sm' color='neutral'>
Get instant notifications when chores are updated
Get instant notifications when tasks are updated
</Typography>
</Box>
</Box>
@@ -181,7 +180,7 @@ const RealTimeSettings = () => {
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
Real-time updates are not available in the Basic plan. Upgrade to Plus
to receive instant notifications when you or other circle members
complete, skip, or modify chores.
complete, skip, or modify tasks.
</Typography>
)}
</Card>

View File

@@ -13,19 +13,47 @@ import moment from 'moment'
import { useEffect, useState } from 'react'
import { useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import {
CreateLongLiveToken,
DeleteLongLiveToken,
GetLongLiveTokens,
} from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import TextModal from '../Modals/Inputs/TextModal'
const APITokenSettings = () => {
const { data: userProfile } = useUserProfile()
const { showNotification } = useNotification()
const [tokens, setTokens] = useState([])
const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
const [showTokenId, setShowTokenId] = useState(null)
const [confirmModalConfig, setConfirmModalConfig] = useState({})
const showConfirmation = (
message,
title,
onConfirm,
confirmText = 'Confirm',
cancelText = 'Cancel',
color = 'primary',
) => {
setConfirmModalConfig({
isOpen: true,
message,
title,
confirmText,
cancelText,
color,
onClose: isConfirmed => {
if (isConfirmed) {
onConfirm()
}
setConfirmModalConfig({})
},
})
}
useEffect(() => {
GetLongLiveTokens().then(resp => {
resp.json().then(data => {
@@ -100,18 +128,28 @@ const APITokenSettings = () => {
variant='outlined'
color='danger'
onClick={() => {
const confirmed = confirm(
`Are you sure you want to remove ${token.name} ?`,
showConfirmation(
`Are you sure you want to remove ${token.name}?`,
'Remove Token',
() => {
DeleteLongLiveToken(token.id).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
title: 'Removed',
message: 'API token has been removed',
})
const newTokens = tokens.filter(
t => t.id !== token.id,
)
setTokens(newTokens)
}
})
},
'Remove',
'Cancel',
'danger',
)
if (confirmed) {
DeleteLongLiveToken(token.id).then(resp => {
if (resp.ok) {
alert('Token removed')
const newTokens = tokens.filter(t => t.id !== token.id)
setTokens(newTokens)
}
})
}
}}
>
Remove
@@ -130,7 +168,10 @@ const APITokenSettings = () => {
color='primary'
onClick={() => {
navigator.clipboard.writeText(token.token)
alert('Token copied to clipboard')
showNotification({
type: 'success',
message: 'Token copied to clipboard',
})
setShowTokenId(null)
}}
>
@@ -166,6 +207,11 @@ const APITokenSettings = () => {
okText={'Generate Token'}
onSave={handleSaveToken}
/>
{/* Modals */}
{confirmModalConfig?.isOpen && (
<ConfirmationModal config={confirmModalConfig} />
)}
</div>
)
}

View File

@@ -34,6 +34,7 @@ import {
UpdatePassword,
} from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
import APITokenSettings from './APITokenSettings'
import MFASettings from './MFASettings'
@@ -41,9 +42,11 @@ import NotificationSetting from './NotificationSetting'
import ProfileSettings from './ProfileSettings'
import StorageSettings from './StorageSettings'
import ThemeToggle from './ThemeToggle'
import { useNotification } from '../../service/NotificationProvider'
const Settings = () => {
const { data: userProfile } = useUserProfile()
const { showNotification } = useNotification()
const [userCircles, setUserCircles] = useState([])
const [circleMemberRequests, setCircleMemberRequests] = useState([])
@@ -54,6 +57,31 @@ const Settings = () => {
const [isAdmin, setIsAdmin] = useState(false)
const [changePasswordModal, setChangePasswordModal] = useState(false)
const [confirmModalConfig, setConfirmModalConfig] = useState({})
const showConfirmation = (
message,
title,
onConfirm,
confirmText = 'Confirm',
cancelText = 'Cancel',
color = 'primary',
) => {
setConfirmModalConfig({
isOpen: true,
message,
title,
confirmText,
cancelText,
color,
onClose: isConfirmed => {
if (isConfirmed) {
onConfirm()
}
setConfirmModalConfig({})
},
})
}
useEffect(() => {
GetUserCircle().then(resp => {
resp.json().then(data => {
@@ -165,7 +193,10 @@ const Settings = () => {
variant='soft'
onClick={() => {
navigator.clipboard.writeText(userCircles[0]?.invite_code)
alert('Code Copied to clipboard')
showNotification({
type: 'success',
message: 'Code copied to clipboard',
})
}}
>
Copy Code
@@ -180,27 +211,42 @@ const Settings = () => {
window.location.host +
`/circle/join?code=${userCircles[0]?.invite_code}`,
)
alert('Link Copied to clipboard')
showNotification({
type: 'success',
message: 'Link copied to clipboard',
})
}}
>
Copy Link
</Button>
{userCircles.length > 0 && userCircles[0]?.userRole === 'member' && (
<Button
color='danger'
variant='outlined'
sx={{ ml: 1 }}
onClick={() => {
const confirmed = confirm(
`Are you sure you want to leave your circle?`,
showConfirmation(
'Are you sure you want to leave your circle?',
'Leave Circle',
() => {
LeaveCircle(userCircles[0]?.id).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
message: 'Left circle successfully',
})
} else {
showNotification({
type: 'error',
message: 'Failed to leave circle',
})
}
})
},
'Leave',
'Cancel',
'danger',
)
if (confirmed) {
LeaveCircle(userCircles[0]?.id).then(resp => {
if (resp.ok) {
alert('Left circle successfully.')
} else {
alert('Failed to leave circle.')
}
})
}
}}
>
Leave Circle
@@ -257,7 +303,10 @@ const Settings = () => {
})
setCircleMembers(newCircleMembers)
} else {
alert('Failed to update role')
showNotification({
type: 'error',
message: 'Failed to update role',
})
}
})
}}
@@ -318,19 +367,26 @@ const Settings = () => {
color='danger'
size='sm'
onClick={() => {
const confirmed = confirm(
showConfirmation(
`Are you sure you want to remove ${member.displayName} from your circle?`,
'Remove Member',
() => {
DeleteCircleMember(
member.circleId,
member.userId,
).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
message: 'Removed member successfully',
})
}
})
},
'Remove',
'Cancel',
'danger',
)
if (confirmed) {
DeleteCircleMember(
member.circleId,
member.userId,
).then(resp => {
if (resp.ok) {
alert('Removed member successfully.')
}
})
}
}}
>
Remove
@@ -353,18 +409,24 @@ const Settings = () => {
variant='soft'
color='success'
onClick={() => {
const confirmed = confirm(
`Are you sure you want to accept ${request.displayName}(username:${request.username}) to join your circle?`,
showConfirmation(
`Are you sure you want to accept ${request.displayName} (username: ${request.username}) to join your circle?`,
'Accept Member Request',
() => {
AcceptCircleMemberRequest(request.id).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
message: 'Accepted request successfully',
})
// reload the page
window.location.reload()
}
})
},
'Accept',
'Cancel',
)
if (confirmed) {
AcceptCircleMemberRequest(request.id).then(resp => {
if (resp.ok) {
alert('Accepted request successfully.')
// reload the page
window.location.reload()
}
})
}
}}
>
Accept
@@ -393,18 +455,23 @@ const Settings = () => {
<Button
variant='soft'
onClick={() => {
const confirmed = confirm(
`Are you sure you want to leave you circle and join '${circleInviteCode}'?`,
showConfirmation(
`Are you sure you want to leave your circle and join '${circleInviteCode}'?`,
'Join Circle',
() => {
JoinCircle(circleInviteCode).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
message:
'Joined circle successfully, wait for the circle owner to accept your request.',
})
}
})
},
'Join',
'Cancel',
)
if (confirmed) {
JoinCircle(circleInviteCode).then(resp => {
if (resp.ok) {
alert(
'Joined circle successfully, wait for the circle owner to accept your request.',
)
}
})
}
}}
>
Join Circle
@@ -479,9 +546,15 @@ const Settings = () => {
onClick={() => {
PutWebhookURL(webhookURL).then(resp => {
if (resp.ok) {
alert('Webhook URL updated successfully.')
showNotification({
type: 'success',
message: 'Webhook URL updated successfully',
})
} else {
alert('Failed to update webhook URL.')
showNotification({
type: 'error',
message: 'Failed to update webhook URL',
})
}
})
}}
@@ -541,10 +614,14 @@ const Settings = () => {
ml: 1,
}}
variant='outlined'
color='danger'
onClick={() => {
CancelSubscription().then(resp => {
if (resp.ok) {
alert('Subscription cancelled.')
showNotification({
type: 'success',
message: 'Subscription cancelled',
})
window.location.reload()
}
})
@@ -575,9 +652,15 @@ const Settings = () => {
if (password) {
UpdatePassword(password).then(resp => {
if (resp.ok) {
alert('Password changed successfully')
showNotification({
type: 'success',
message: 'Password changed successfully',
})
} else {
alert('Password change failed')
showNotification({
type: 'error',
message: 'Password change failed',
})
}
})
}
@@ -601,6 +684,11 @@ const Settings = () => {
</Typography>
<ThemeToggle />
</div>
{/* Modals */}
{confirmModalConfig?.isOpen && (
<ConfirmationModal config={confirmModalConfig} />
)}
</Container>
)
}

View File

@@ -12,12 +12,38 @@ import { useNavigate } from 'react-router-dom'
import { useUserProfile } from '../../queries/UserQueries'
import { GetStorageUsage } from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
const StorageSettings = () => {
const Navigate = useNavigate()
const { data: userProfile } = useUserProfile()
const [usage, setUsage] = useState({ used: 0, total: 0 })
const [loading, setLoading] = useState(true)
const [confirmModalConfig, setConfirmModalConfig] = useState({})
const showConfirmation = (
message,
title,
onConfirm,
confirmText = 'Confirm',
cancelText = 'Cancel',
color = 'primary',
) => {
setConfirmModalConfig({
isOpen: true,
message,
title,
confirmText,
cancelText,
color,
onClose: isConfirmed => {
if (isConfirmed) {
onConfirm()
}
setConfirmModalConfig({})
},
})
}
useEffect(() => {
if (isPlusAccount(userProfile)) {
@@ -101,13 +127,17 @@ const StorageSettings = () => {
variant='soft'
color='danger'
onClick={() => {
const confirmed = confirm(
`Are you sure you want to clear your local storage and cache? This will remove all your data from this browser and require login.`,
showConfirmation(
'Are you sure you want to clear your local storage and cache? This will remove all your data from this browser and require login.',
'Clear All Local Storage',
() => {
localStorage.clear()
Navigate('/login')
},
'Clear All',
'Cancel',
'danger',
)
if (confirmed) {
localStorage.clear()
Navigate('/login')
}
}}
>
Clear All Local Storage and Cache
@@ -116,20 +146,29 @@ const StorageSettings = () => {
variant='outlined'
color='danger'
onClick={() => {
const confirmed = confirm(
`Are you sure you want to clear only the offline cache and tasks?`,
showConfirmation(
'Are you sure you want to clear only the offline cache and tasks?',
'Clear Offline Cache',
() => {
localStorage.removeItem('offline_cache')
localStorage.removeItem('offline_request_queue')
localStorage.removeItem('offlineTasks')
},
'Clear Cache',
'Cancel',
'danger',
)
if (confirmed) {
localStorage.removeItem('offline_cache')
localStorage.removeItem('offline_request_queue')
localStorage.removeItem('offlineTasks')
}
}}
sx={{ mt: 1 }}
>
Clear Offline Cache and Offline Tasks
</Button>
</Card>
{/* Modals */}
{confirmModalConfig?.isOpen && (
<ConfirmationModal config={confirmModalConfig} />
)}
</div>
)
}