refactor: Remove unused user profile references and improve SSE/WebSocket connection logic
This commit is contained in:
@@ -1,46 +1,26 @@
|
||||
import { Sync, SyncDisabled } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Option,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { Box, Card, Chip, FormHelperText, Switch, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useWebSocketContext } from '../contexts/WebSocketContext'
|
||||
import { useSSEContext } from '../hooks/useSSEContext'
|
||||
import { useUserProfile } from '../queries/UserQueries'
|
||||
import { isPlusAccount } from '../utils/Helpers'
|
||||
import SSEConnectionStatus from './SSEConnectionStatus'
|
||||
import WebSocketConnectionStatus from './WebSocketConnectionStatus'
|
||||
|
||||
const REALTIME_TYPES = {
|
||||
DISABLED: 'disabled',
|
||||
WEBSOCKET: 'websocket',
|
||||
SSE: 'sse',
|
||||
}
|
||||
|
||||
const RealTimeSettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
// WebSocket context
|
||||
const webSocketContext = useWebSocketContext()
|
||||
|
||||
// SSE context
|
||||
const sseContext = useSSEContext()
|
||||
|
||||
// Get current realtime type from localStorage
|
||||
const getCurrentRealtimeType = () => {
|
||||
const wsEnabled = localStorage.getItem('websocket_enabled') !== 'false'
|
||||
const sseEnabled = localStorage.getItem('sse_enabled') === 'true'
|
||||
|
||||
if (sseEnabled) return REALTIME_TYPES.SSE
|
||||
if (wsEnabled) return REALTIME_TYPES.WEBSOCKET
|
||||
return REALTIME_TYPES.DISABLED
|
||||
return sseEnabled ? REALTIME_TYPES.SSE : REALTIME_TYPES.DISABLED
|
||||
}
|
||||
|
||||
const [realtimeType, setRealtimeType] = useState(getCurrentRealtimeType())
|
||||
@@ -55,21 +35,11 @@ const RealTimeSettings = () => {
|
||||
// Update localStorage and toggle connections
|
||||
switch (newValue) {
|
||||
case REALTIME_TYPES.DISABLED:
|
||||
localStorage.setItem('websocket_enabled', 'false')
|
||||
localStorage.setItem('sse_enabled', 'false')
|
||||
webSocketContext.disconnect()
|
||||
sseContext.disconnect()
|
||||
break
|
||||
case REALTIME_TYPES.WEBSOCKET:
|
||||
localStorage.setItem('websocket_enabled', 'true')
|
||||
localStorage.setItem('sse_enabled', 'false')
|
||||
sseContext.disconnect()
|
||||
webSocketContext.connect()
|
||||
break
|
||||
case REALTIME_TYPES.SSE:
|
||||
localStorage.setItem('websocket_enabled', 'false')
|
||||
localStorage.setItem('sse_enabled', 'true')
|
||||
webSocketContext.disconnect()
|
||||
sseContext.connect()
|
||||
break
|
||||
}
|
||||
@@ -77,8 +47,6 @@ const RealTimeSettings = () => {
|
||||
|
||||
const getCurrentContext = () => {
|
||||
switch (realtimeType) {
|
||||
case REALTIME_TYPES.WEBSOCKET:
|
||||
return webSocketContext
|
||||
case REALTIME_TYPES.SSE:
|
||||
return sseContext
|
||||
default:
|
||||
@@ -99,31 +67,26 @@ const RealTimeSettings = () => {
|
||||
}
|
||||
|
||||
if (realtimeType === REALTIME_TYPES.DISABLED) {
|
||||
return 'Real-time updates are disabled. Enable WebSocket or SSE 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 chores.'
|
||||
}
|
||||
|
||||
const typeLabel =
|
||||
realtimeType === REALTIME_TYPES.WEBSOCKET ? 'WebSocket' : 'SSE'
|
||||
|
||||
if (context.isConnected) {
|
||||
return `Real-time updates (${typeLabel}) 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 chores."
|
||||
}
|
||||
|
||||
if (context.isConnecting) {
|
||||
return `Connecting to real-time updates (${typeLabel})...`
|
||||
return 'Connecting to real-time updates...'
|
||||
}
|
||||
|
||||
if (context.error) {
|
||||
return `Real-time updates (${typeLabel}) are enabled but not working: ${context.error}`
|
||||
return `Real-time updates are enabled but not working: ${context.error}`
|
||||
}
|
||||
|
||||
return `Real-time updates (${typeLabel}) are enabled but not currently connected.`
|
||||
return 'Real-time updates are enabled but not currently connected.'
|
||||
}
|
||||
|
||||
const getConnectionStatusComponent = () => {
|
||||
switch (realtimeType) {
|
||||
case REALTIME_TYPES.WEBSOCKET:
|
||||
return <WebSocketConnectionStatus variant='chip' />
|
||||
case REALTIME_TYPES.SSE:
|
||||
return <SSEConnectionStatus variant='chip' />
|
||||
default:
|
||||
@@ -133,7 +96,7 @@ const RealTimeSettings = () => {
|
||||
|
||||
return (
|
||||
<Card sx={{ mt: 2, p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2, mb: 2 }}>
|
||||
{realtimeType !== REALTIME_TYPES.DISABLED &&
|
||||
isPlusAccount(userProfile) ? (
|
||||
<Sync color={context.isConnected ? 'success' : 'disabled'} />
|
||||
@@ -141,24 +104,44 @@ const RealTimeSettings = () => {
|
||||
<SyncDisabled color='disabled' />
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='title-md'>
|
||||
Real-time Updates
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography level='title-md'>
|
||||
Real-time Updates
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</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' }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Get instant notifications when chores are updated
|
||||
</Typography>
|
||||
</Box>
|
||||
{realtimeType !== REALTIME_TYPES.DISABLED &&
|
||||
isPlusAccount(userProfile) &&
|
||||
getConnectionStatusComponent()}
|
||||
</Box>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ mb: 2 }}>
|
||||
{/* <FormControl orientation='horizontal' sx={{ mb: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<FormLabel>Real-time Connection Type</FormLabel>
|
||||
<FormHelperText sx={{ mt: 0 }}>
|
||||
@@ -175,7 +158,7 @@ const RealTimeSettings = () => {
|
||||
<Option value={REALTIME_TYPES.WEBSOCKET}>WebSocket</Option>
|
||||
<Option value={REALTIME_TYPES.SSE}>SSE</Option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormControl> */}
|
||||
|
||||
<FormHelperText sx={{ mb: 2 }}>{getStatusDescription()}</FormHelperText>
|
||||
|
||||
@@ -185,19 +168,7 @@ const RealTimeSettings = () => {
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Status:
|
||||
</Typography>
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={
|
||||
context.isConnected
|
||||
? 'success'
|
||||
: context.isConnecting
|
||||
? 'warning'
|
||||
: 'danger'
|
||||
}
|
||||
>
|
||||
{context.getConnectionStatus()}
|
||||
</Chip>
|
||||
{getConnectionStatusComponent()}
|
||||
{context.error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{context.error}
|
||||
@@ -213,30 +184,6 @@ const RealTimeSettings = () => {
|
||||
complete, skip, or modify chores.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{realtimeType !== REALTIME_TYPES.DISABLED &&
|
||||
isPlusAccount(userProfile) && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
p: 2,
|
||||
bgcolor: 'background.level1',
|
||||
borderRadius: 'sm',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 'bold', mb: 1 }}>
|
||||
Connection Types:
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ mb: 1 }}>
|
||||
• <strong>WebSocket:</strong> Traditional bi-directional real-time
|
||||
connection
|
||||
</Typography>
|
||||
<Typography level='body-xs'>
|
||||
• <strong>SSE:</strong> Server-Sent Events - lighter weight,
|
||||
one-way updates from server
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useUserProfile } from '../queries/UserQueries'
|
||||
import { isPlusAccount } from '../utils/Helpers'
|
||||
import { apiManager, isTokenValid } from '../utils/TokenManager'
|
||||
|
||||
const SSE_STATES = {
|
||||
@@ -29,16 +27,8 @@ export const useSSE = () => {
|
||||
const heartbeatMonitorRef = useRef(null)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const getSSEUrl = useCallback(() => {
|
||||
if (!userProfile?.circleID) {
|
||||
console.log(
|
||||
'SSE: User not part of any circle - real-time features unavailable',
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('ca_token')
|
||||
if (!token || !isTokenValid()) {
|
||||
console.log('SSE: No valid authentication token')
|
||||
@@ -48,12 +38,11 @@ export const useSSE = () => {
|
||||
// Get the API URL from apiManager
|
||||
const apiUrl = apiManager.getApiURL() // e.g., "http://localhost:8080/api/v1"
|
||||
|
||||
// Build SSE URL
|
||||
const sseUrl = `${apiUrl}/realtime/sse?circleId=${userProfile.circleID}`
|
||||
// Build SSE URL - let backend determine circle from authenticated user
|
||||
const sseUrl = `${apiUrl}/realtime/sse`
|
||||
|
||||
console.log('SSE: Generated URL:', sseUrl)
|
||||
return { url: sseUrl, token }
|
||||
}, [userProfile])
|
||||
}, [])
|
||||
|
||||
const handleSSEMessage = useCallback(
|
||||
event => {
|
||||
@@ -61,8 +50,6 @@ export const useSSE = () => {
|
||||
const eventData = JSON.parse(event.data)
|
||||
setLastEvent(eventData)
|
||||
|
||||
console.log('SSE event received:', eventData.type, eventData)
|
||||
|
||||
// Update heartbeat timestamp
|
||||
if (eventData.type === 'heartbeat') {
|
||||
lastHeartbeatRef.current = Date.now()
|
||||
@@ -152,14 +139,8 @@ export const useSSE = () => {
|
||||
|
||||
// Create connect function that can be called from anywhere
|
||||
const connect = useCallback(() => {
|
||||
console.log('SSE connect called')
|
||||
console.log('SSE current state:', eventSourceRef.current?.readyState)
|
||||
|
||||
// Circuit breaker: prevent infinite reconnection loops
|
||||
if (isCircuitBreakerOpen) {
|
||||
console.warn(
|
||||
'SSE: Circuit breaker is open, preventing connection attempt',
|
||||
)
|
||||
console.log('SSE: Circuit breaker is open, preventing connection attempt')
|
||||
setError(
|
||||
'Connection blocked due to repeated failures. Please try again later.',
|
||||
)
|
||||
@@ -209,8 +190,9 @@ export const useSSE = () => {
|
||||
setConnectionState(SSE_STATES.CONNECTING)
|
||||
isManuallyClosedRef.current = false
|
||||
|
||||
// Option 1: Use EventSource polyfill with Authorization header (Recommended)
|
||||
// This is the most secure and standard way to authenticate SSE connections
|
||||
// here use EventSource polyfill with Authorization header as the native EventSource does not support headers
|
||||
// the other option was to pass via query param which is less secure and also there.
|
||||
// TODO: use cookie-based once/if at all i move from local storage to httpOnly cookies.
|
||||
eventSourceRef.current = new EventSourcePolyfill(sseConfig.url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${sseConfig.token}`,
|
||||
@@ -306,7 +288,7 @@ export const useSSE = () => {
|
||||
setError('Failed to establish connection')
|
||||
setConnectionState(SSE_STATES.CLOSED)
|
||||
}
|
||||
}, [getSSEUrl, handleSSEMessage, stopHeartbeatMonitor])
|
||||
}, [getSSEUrl, handleSSEMessage, stopHeartbeatMonitor, isCircuitBreakerOpen])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
isManuallyClosedRef.current = true
|
||||
@@ -330,12 +312,10 @@ export const useSSE = () => {
|
||||
enabled => {
|
||||
console.log('SSE toggleSSEEnabled called:', {
|
||||
enabled,
|
||||
userProfile: userProfile?.circleID,
|
||||
isTokenValid: isTokenValid(),
|
||||
isPlusAccount: isPlusAccount(userProfile),
|
||||
})
|
||||
localStorage.setItem('sse_enabled', enabled.toString())
|
||||
if (enabled && userProfile?.circleID && isTokenValid()) {
|
||||
if (enabled && isTokenValid()) {
|
||||
console.log('SSE toggleSSEEnabled: Calling connect()')
|
||||
connect()
|
||||
} else {
|
||||
@@ -343,38 +323,27 @@ export const useSSE = () => {
|
||||
disconnect()
|
||||
}
|
||||
},
|
||||
[connect, disconnect, userProfile],
|
||||
[connect, disconnect],
|
||||
)
|
||||
|
||||
const isSSEEnabled = useCallback(() => {
|
||||
return localStorage.getItem('sse_enabled') === 'true'
|
||||
}, [])
|
||||
|
||||
// Auto-connect when user profile is available and token is valid
|
||||
// Auto-connect when SSE is enabled and token is valid
|
||||
useEffect(() => {
|
||||
console.log('SSE auto-connect effect triggered')
|
||||
console.log('UserProfile:', userProfile)
|
||||
console.log('circleID:', userProfile?.circleID)
|
||||
console.log('Token valid:', isTokenValid())
|
||||
console.log('Is Plus account:', isPlusAccount(userProfile))
|
||||
|
||||
// Check if SSE is enabled in settings
|
||||
const isSSEEnabledSetting = localStorage.getItem('sse_enabled') === 'true'
|
||||
console.log('SSE enabled in settings:', isSSEEnabledSetting)
|
||||
|
||||
if (
|
||||
userProfile?.circleID &&
|
||||
isTokenValid() &&
|
||||
isSSEEnabledSetting &&
|
||||
isPlusAccount(userProfile)
|
||||
) {
|
||||
if (isTokenValid() && isSSEEnabledSetting) {
|
||||
console.log('SSE: Conditions met, attempting to connect')
|
||||
connect()
|
||||
} else {
|
||||
console.log('SSE: Conditions not met, disconnecting')
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
console.log('SSE: Not a Plus account - feature unavailable')
|
||||
}
|
||||
disconnect()
|
||||
}
|
||||
|
||||
@@ -383,7 +352,7 @@ export const useSSE = () => {
|
||||
disconnect()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userProfile?.circleID, userProfile?.expiration]) // Only depend on essential userProfile fields
|
||||
}, []) // Only run once on mount
|
||||
|
||||
// Cleanup timeouts on unmount
|
||||
useEffect(() => {
|
||||
@@ -410,10 +379,8 @@ export const useSSE = () => {
|
||||
const isSSEEnabledSetting =
|
||||
localStorage.getItem('sse_enabled') === 'true'
|
||||
if (
|
||||
userProfile?.circleID &&
|
||||
isTokenValid() &&
|
||||
isSSEEnabledSetting &&
|
||||
isPlusAccount(userProfile) &&
|
||||
connectionState !== SSE_STATES.OPEN
|
||||
) {
|
||||
connect()
|
||||
@@ -426,8 +393,7 @@ export const useSSE = () => {
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userProfile?.circleID, userProfile?.expiration, connectionState])
|
||||
}, [connectionState, connect])
|
||||
|
||||
return {
|
||||
connectionState,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useUserProfile } from '../queries/UserQueries'
|
||||
import { isPlusAccount } from '../utils/Helpers'
|
||||
import { apiManager, isTokenValid } from '../utils/TokenManager'
|
||||
|
||||
const WEBSOCKET_STATES = {
|
||||
@@ -26,16 +24,8 @@ export const useWebSocket = () => {
|
||||
const isManuallyClosedRef = useRef(false)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const getWebSocketUrl = useCallback(() => {
|
||||
if (!userProfile?.circleID) {
|
||||
console.log(
|
||||
'WebSocket: User not part of any circle - real-time features unavailable',
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('ca_token')
|
||||
if (!token || !isTokenValid()) {
|
||||
console.log('WebSocket: No valid authentication token')
|
||||
@@ -55,10 +45,11 @@ export const useWebSocket = () => {
|
||||
wsUrl = `${isHttps ? 'wss:' : 'ws:'}//${wsUrl}`
|
||||
}
|
||||
|
||||
wsUrl = `${wsUrl}/api/v1/realtime/ws?token=${token}&circleId=${userProfile.circleID}`
|
||||
// Let backend determine circle from authenticated user
|
||||
wsUrl = `${wsUrl}/api/v1/realtime/ws?token=${token}`
|
||||
|
||||
return wsUrl
|
||||
}, [userProfile])
|
||||
}, [])
|
||||
|
||||
const handleWebSocketMessage = useCallback(
|
||||
event => {
|
||||
@@ -250,39 +241,31 @@ export const useWebSocket = () => {
|
||||
const toggleWebSocketEnabled = useCallback(
|
||||
enabled => {
|
||||
localStorage.setItem('websocket_enabled', enabled.toString())
|
||||
if (enabled && userProfile?.circleID && isTokenValid()) {
|
||||
if (enabled && isTokenValid()) {
|
||||
connect()
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
},
|
||||
[connect, disconnect, userProfile],
|
||||
[connect, disconnect],
|
||||
)
|
||||
|
||||
const isWebSocketEnabled = useCallback(() => {
|
||||
return localStorage.getItem('websocket_enabled') !== 'false'
|
||||
}, [])
|
||||
|
||||
// Auto-connect when user profile is available and token is valid
|
||||
// Auto-connect when WebSocket is enabled and token is valid
|
||||
useEffect(() => {
|
||||
// Check if WebSocket is enabled in settings
|
||||
const isWebSocketEnabledSetting =
|
||||
localStorage.getItem('websocket_enabled') !== 'false'
|
||||
console.log('WebSocket enabled in settings:', isWebSocketEnabledSetting)
|
||||
|
||||
if (
|
||||
userProfile?.circleID &&
|
||||
isTokenValid() &&
|
||||
isWebSocketEnabledSetting &&
|
||||
isPlusAccount(userProfile)
|
||||
) {
|
||||
if (isTokenValid() && isWebSocketEnabledSetting) {
|
||||
console.log('WebSocket: Conditions met, attempting to connect')
|
||||
connect()
|
||||
} else {
|
||||
console.log('WebSocket: Conditions not met, disconnecting')
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
console.log('WebSocket: Not a Plus account - feature unavailable')
|
||||
}
|
||||
disconnect()
|
||||
}
|
||||
|
||||
@@ -290,7 +273,8 @@ export const useWebSocket = () => {
|
||||
return () => {
|
||||
disconnect()
|
||||
}
|
||||
}, [userProfile, connect, disconnect])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []) // Only run once on mount
|
||||
|
||||
// Cleanup timeouts on unmount
|
||||
useEffect(() => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import { LoginSocialGoogle } from 'reactjs-social-login'
|
||||
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'
|
||||
@@ -29,7 +28,7 @@ import MFAVerificationModal from './MFAVerificationModal'
|
||||
|
||||
const LoginView = () => {
|
||||
// Only fetch user profile if token is valid to prevent unnecessary queries
|
||||
const { data: userProfileData } = useUserProfile()
|
||||
// const { data: userProfileData } = useUserProfile()
|
||||
const [userProfile, setUserProfile] = useState(null)
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
@@ -7,9 +7,6 @@ import {
|
||||
Chip,
|
||||
FormControl,
|
||||
Input,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemDecorator,
|
||||
Option,
|
||||
Select,
|
||||
TextField,
|
||||
@@ -113,7 +110,7 @@ const ThingTriggerSection = ({
|
||||
onChange={(e, newValue) => setSelectedThing(newValue)}
|
||||
getOptionLabel={option => option.name}
|
||||
renderOption={(props, option) => (
|
||||
<ListItem {...props}>
|
||||
<Box {...props}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -123,19 +120,19 @@ const ThingTriggerSection = ({
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator sx={{ alignSelf: 'flex-start' }}>
|
||||
<Box sx={{ alignSelf: 'flex-start' }}>
|
||||
<Typography level='body-lg' textColor='primary'>
|
||||
{option.name}
|
||||
</Typography>
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography level='body2' textColor='text.secondary'>
|
||||
<Chip>type: {option.type}</Chip>{' '}
|
||||
<Chip>state: {option.state}</Chip>
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</Box>
|
||||
)}
|
||||
renderInput={params => (
|
||||
<TextField {...params} label='Select a thing' />
|
||||
|
||||
@@ -110,7 +110,7 @@ const MultiSelectHelp = ({ isVisible = true }) => {
|
||||
description='Mark selected tasks as completed'
|
||||
/>
|
||||
<ShortcutItem
|
||||
keys={['Del']}
|
||||
keys={['Del', '⌫']}
|
||||
description='Delete selected tasks'
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -825,18 +825,6 @@ const MyChores = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{JSON.stringify(userProfile) === 'null'}
|
||||
</Typography>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{userLabelsLoading}
|
||||
</Typography>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{performers.length === 0}
|
||||
</Typography>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{choresLoading}
|
||||
</Typography>
|
||||
<LoadingComponent />
|
||||
</>
|
||||
)
|
||||
@@ -1227,7 +1215,6 @@ const MyChores = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Primary Actions - Safe operations */}
|
||||
<Button
|
||||
size='sm'
|
||||
variant='solid'
|
||||
@@ -1237,15 +1224,13 @@ const MyChores = () => {
|
||||
disabled={selectedChores.size === 0}
|
||||
sx={{
|
||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||
fontWeight: 'md',
|
||||
}}
|
||||
>
|
||||
Complete
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
variant='soft'
|
||||
color='warning'
|
||||
onClick={handleBulkSkip}
|
||||
startDecorator={<SkipNext />}
|
||||
@@ -1256,48 +1241,29 @@ const MyChores = () => {
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
|
||||
{/* Visual separator for destructive actions */}
|
||||
<Divider
|
||||
orientation='vertical'
|
||||
sx={{
|
||||
height: '24px',
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
mx: 0.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Secondary Actions - Less destructive */}
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={handleBulkArchive}
|
||||
startDecorator={<Archive />}
|
||||
disabled={selectedChores.size === 0}
|
||||
sx={{
|
||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||
borderStyle: 'dashed',
|
||||
}}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
|
||||
{/* Most destructive action - visually distinct */}
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={handleBulkDelete}
|
||||
startDecorator={<Delete />}
|
||||
disabled={selectedChores.size === 0}
|
||||
sx={{
|
||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||
borderWidth: '2px',
|
||||
'&:hover': {
|
||||
borderWidth: '2px',
|
||||
backgroundColor: 'danger.softBg',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
|
||||
@@ -165,7 +165,6 @@ const CalendarView = ({ chores }) => {
|
||||
return legendItems.map((item, index) => (
|
||||
<Grid
|
||||
key={index}
|
||||
item
|
||||
xs={12}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
||||
Reference in New Issue
Block a user