diff --git a/src/hooks/useSSE.js b/src/hooks/useSSE.js
index 55c009e..cbebd5f 100644
--- a/src/hooks/useSSE.js
+++ b/src/hooks/useSSE.js
@@ -1,6 +1,7 @@
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 { useAlerts } from '../service/AlertsProvider'
import { useNotification } from '../service/NotificationProvider'
import { apiManager, isTokenValid } from '../utils/TokenManager'
@@ -15,6 +16,7 @@ const MAX_RECONNECT_ATTEMPTS = 10 // Circuit breaker limit
const CIRCUIT_BREAKER_RESET_TIME = 600000 // 10 minutes
export const useSSE = () => {
+ const { data: userProfile } = useUserProfile()
const [connectionState, setConnectionState] = useState(SSE_STATES.CLOSED)
const [lastEvent, setLastEvent] = useState(null)
const [error, setError] = useState(null)
@@ -65,12 +67,14 @@ export const useSSE = () => {
case 'chore.updated':
case 'chore.completed':
case 'chore.skipped': {
- showNotification({
- type: 'info',
- title: `Task ${eventData.type.replace('chore.', '')}`,
- message: `${eventData.data.user.displayName} ${eventData.type.replace('chore.', '')} "${eventData.data.chore.name}"`,
- duration: 5000,
- })
+ if (eventData?.data?.user?.id !== userProfile?.id) {
+ showNotification({
+ type: 'info',
+ title: `Task ${eventData.type.replace('chore.', '')}`,
+ message: `${eventData.data.user.displayName} ${eventData.type.replace('chore.', '')} "${eventData.data.chore.name}"`,
+ duration: 5000,
+ })
+ }
const updatedChore = eventData.data.chore
// Update individual chore cache
diff --git a/src/service/AlertsProvider.jsx b/src/service/AlertsProvider.jsx
index 6f29486..0815f3d 100644
--- a/src/service/AlertsProvider.jsx
+++ b/src/service/AlertsProvider.jsx
@@ -43,11 +43,12 @@ export const AlertsProvider = ({ children }) => {
{visibleAlert && (
diff --git a/src/views/ChoreEdit/TimePassedCard.jsx b/src/views/ChoreEdit/TimePassedCard.jsx
index ae2a195..e1d5a0c 100644
--- a/src/views/ChoreEdit/TimePassedCard.jsx
+++ b/src/views/ChoreEdit/TimePassedCard.jsx
@@ -1,8 +1,16 @@
-import { Flag, Pause, PlayArrow, Schedule } from '@mui/icons-material'
+import {
+ Flag,
+ OpenInFull,
+ Pause,
+ PlayArrow,
+ Schedule,
+} from '@mui/icons-material'
import { Box, Card, Chip, Typography } from '@mui/joy'
import { useEffect, useRef, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
+ const navigate = useNavigate()
const [time, setTime] = useState(0)
const [shouldAnimate, setShouldAnimate] = useState(false)
const [prevStatus, setPrevStatus] = useState(null) // Initialize as null
@@ -96,8 +104,30 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
},
},
transition: 'all 0.3s ease',
+ cursor: 'pointer',
+ }}
+ onClick={e => {
+ // if this click on this element itself and not its children:
+ if (e.target !== e.currentTarget) return
+ navigate('./timer')
}}
>
+ {
+ e.stopPropagation()
+ navigate('./timer')
+ }}
+ >
Timer Details
- */}
Clear & Reset
diff --git a/src/views/Timer/TimerDetails.jsx b/src/views/Timer/TimerDetails.jsx
index 710677b..45bc99b 100644
--- a/src/views/Timer/TimerDetails.jsx
+++ b/src/views/Timer/TimerDetails.jsx
@@ -18,30 +18,46 @@ import {
FormControl,
FormHelperText,
Grid,
+ IconButton,
Input,
Typography,
} from '@mui/joy'
import moment from 'moment'
-import { useEffect, useState } from 'react'
-import { useNavigate, useParams } from 'react-router-dom'
+import { useEffect, useRef, useState } from 'react'
+import { useParams } from 'react-router-dom'
import { useNotification } from '../../service/NotificationProvider'
import {
- DeleteTimeSession,
GetChoreTimer,
+ PauseChore,
+ StartChore,
UpdateTimeSession,
} from '../../utils/Fetcher'
-import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
const TimerDetails = () => {
const { choreId } = useParams()
- const navigate = useNavigate()
const [timerData, setTimerData] = useState(null)
const [loading, setLoading] = useState(false)
const [editingSessions, setEditingSessions] = useState({})
- const [confirmDeleteConfig, setConfirmDeleteConfig] = useState({})
const [currentTime, setCurrentTime] = useState(new Date())
+ const [timerActionLoading, setTimerActionLoading] = useState(false)
const { showError, showSuccess } = useNotification()
+ // Swipe functionality state for session cards
+ const [sessionSwipeStates, setSessionSwipeStates] = useState({})
+ const swipeThreshold = 80
+ const maxSwipeDistance = 160
+ const dragStartX = useRef(0)
+ const [isDragging, setIsDragging] = useState(false)
+ const [isTouchDevice, setIsTouchDevice] = useState(false)
+
+ // Detect if device supports touch
+ useEffect(() => {
+ const checkTouchDevice = () => {
+ setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0)
+ }
+ checkTouchDevice()
+ }, [])
+
// Fetch timer data when component mounts
useEffect(() => {
if (choreId) {
@@ -227,53 +243,63 @@ const TimerDetails = () => {
}
}
- const deleteSession = async sessionId => {
- setLoading(true)
+ // Timer control functions
+ const handleStartTimer = async () => {
+ setTimerActionLoading(true)
try {
- const response = await DeleteTimeSession(choreId, sessionId)
+ const response = await StartChore(choreId)
if (response.ok) {
showSuccess({
- title: 'Session deleted',
- message: 'Timer session has been deleted successfully.',
+ title: 'Timer Started',
+ message: 'Work session has been started successfully.',
})
await fetchTimerData()
- // Navigate back after successful deletion
- navigate(`/chores/${choreId}`)
} else {
showError({
- title: 'Failed to delete session',
+ title: 'Failed to start timer',
message: 'Please try again.',
})
}
} catch (error) {
showError({
- title: 'Error deleting session',
+ title: 'Error starting timer',
message: error.message,
})
} finally {
- setLoading(false)
+ setTimerActionLoading(false)
}
}
- const confirmDeleteSession = sessionId => {
- setConfirmDeleteConfig({
- isOpen: true,
- title: 'Delete Timer Session',
- message: 'Are you sure you want to delete this timer session?',
- confirmText: 'Delete',
- cancelText: 'Cancel',
- color: 'danger',
- onClose: isConfirmed => {
- if (isConfirmed) {
- deleteSession(sessionId)
- }
- setConfirmDeleteConfig({})
- },
- })
+ const handlePauseTimer = async () => {
+ setTimerActionLoading(true)
+ try {
+ const response = await PauseChore(choreId)
+ if (response.ok) {
+ showSuccess({
+ title: 'Timer Paused',
+ message: 'Work session has been paused.',
+ })
+ await fetchTimerData()
+ } else {
+ showError({
+ title: 'Failed to pause timer',
+ message: 'Please try again.',
+ })
+ }
+ } catch (error) {
+ showError({
+ title: 'Error pausing timer',
+ message: error.message,
+ })
+ } finally {
+ setTimerActionLoading(false)
+ }
}
- const handleGoBack = () => {
- navigate(`/chores/${choreId}`)
+ // Determine if timer is currently running
+ const isTimerRunning = () => {
+ if (!timerData || !timerData.pauseLog) return false
+ return timerData.pauseLog.some(session => session.start && !session.end)
}
// Calculate total duration from start to now/end (real-time)
@@ -318,8 +344,165 @@ const TimerDetails = () => {
return Math.max(0, totalDuration - activeDuration)
}
+ // Swipe functionality methods
+ const getSessionSwipeState = sessionIndex => {
+ return (
+ sessionSwipeStates[sessionIndex] || {
+ translateX: 0,
+ isRevealed: false,
+ }
+ )
+ }
+
+ const updateSessionSwipeState = (sessionIndex, newState) => {
+ setSessionSwipeStates(prev => ({
+ ...prev,
+ [sessionIndex]: {
+ ...prev[sessionIndex],
+ ...newState,
+ },
+ }))
+ }
+
+ const resetSessionSwipe = sessionIndex => {
+ updateSessionSwipeState(sessionIndex, {
+ translateX: 0,
+ isRevealed: false,
+ })
+ }
+
+ const resetAllSwipes = () => {
+ setSessionSwipeStates({})
+ }
+
+ // Touch handlers for swipe
+ const handleSessionTouchStart = e => {
+ dragStartX.current = e.touches[0].clientX
+ setIsDragging(true)
+ }
+
+ const handleSessionTouchMove = (e, sessionIndex) => {
+ if (!isDragging) return
+
+ const currentX = e.touches[0].clientX
+ const deltaX = currentX - dragStartX.current
+ const currentState = getSessionSwipeState(sessionIndex)
+
+ if (currentState.isRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ updateSessionSwipeState(sessionIndex, { translateX: clampedDelta })
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ updateSessionSwipeState(sessionIndex, { translateX: clampedDelta })
+ }
+ }
+ }
+
+ const handleSessionTouchEnd = (e, sessionIndex) => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ const currentState = getSessionSwipeState(sessionIndex)
+
+ if (currentState.isRevealed) {
+ if (currentState.translateX > -swipeThreshold) {
+ resetSessionSwipe(sessionIndex)
+ } else {
+ updateSessionSwipeState(sessionIndex, {
+ translateX: -maxSwipeDistance,
+ isRevealed: true,
+ })
+ }
+ } else {
+ if (Math.abs(currentState.translateX) > swipeThreshold) {
+ updateSessionSwipeState(sessionIndex, {
+ translateX: -maxSwipeDistance,
+ isRevealed: true,
+ })
+ } else {
+ resetSessionSwipe(sessionIndex)
+ }
+ }
+ }
+
+ // Mouse handlers for swipe (desktop)
+ const handleSessionMouseDown = e => {
+ dragStartX.current = e.clientX
+ setIsDragging(true)
+ }
+
+ const handleSessionMouseMove = (e, sessionIndex) => {
+ if (!isDragging) return
+
+ const currentX = e.clientX
+ const deltaX = currentX - dragStartX.current
+ const currentState = getSessionSwipeState(sessionIndex)
+
+ if (currentState.isRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ updateSessionSwipeState(sessionIndex, { translateX: clampedDelta })
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ updateSessionSwipeState(sessionIndex, { translateX: clampedDelta })
+ }
+ }
+ }
+
+ const handleSessionMouseUp = (e, sessionIndex) => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ const currentState = getSessionSwipeState(sessionIndex)
+
+ if (currentState.isRevealed) {
+ if (currentState.translateX > -swipeThreshold) {
+ resetSessionSwipe(sessionIndex)
+ } else {
+ updateSessionSwipeState(sessionIndex, {
+ translateX: -maxSwipeDistance,
+ isRevealed: true,
+ })
+ }
+ } else {
+ if (Math.abs(currentState.translateX) > swipeThreshold) {
+ updateSessionSwipeState(sessionIndex, {
+ translateX: -maxSwipeDistance,
+ isRevealed: true,
+ })
+ } else {
+ resetSessionSwipe(sessionIndex)
+ }
+ }
+ }
+
+ const handleEditSession = () => {
+ resetAllSwipes()
+ // Trigger the existing edit functionality
+ startEditingSession()
+ }
+
+ const handleDeleteSession = sessionIndex => {
+ resetAllSwipes()
+ // For now, just show an alert since we'd need to implement session deletion API
+ showError({
+ title: 'Delete Session',
+ message: `Session #${sessionIndex + 1} deletion would be implemented here`,
+ })
+ }
+
+ // Reset swipes when editing mode changes
+ useEffect(() => {
+ resetAllSwipes()
+ }, [editingSessions])
+
return (
-
+
{/* Header */}
{loading && (
@@ -387,8 +570,8 @@ const TimerDetails = () => {
{
{
{
{
}}
/>
+
+ {/* Timeline Graph */}
+
+
+ Activity Timeline
+
+
+ {timerData &&
+ timerData.pauseLog &&
+ timerData.pauseLog.length > 0 ? (
+
+ {/* Timeline visualization */}
+
+ {(() => {
+ const totalDuration = calculateTotalDuration()
+ const startTime = new Date(timerData.startTime)
+
+ return timerData.pauseLog.map((session, index) => {
+ const sessionStart = new Date(session.start)
+ const sessionEnd = session.end
+ ? new Date(session.end)
+ : currentTime
+
+ // Calculate position and width as percentages
+ const startOffset = Math.max(
+ 0,
+ (sessionStart - startTime) / 1000,
+ )
+ const sessionDuration = Math.max(
+ 0,
+ (sessionEnd - sessionStart) / 1000,
+ )
+
+ const leftPercent =
+ (startOffset / Math.max(totalDuration, 1)) * 100
+ const widthPercent =
+ (sessionDuration / Math.max(totalDuration, 1)) * 100
+
+ const isOngoing = !session.end
+
+ return (
+
+ )
+ })
+ })()}
+
+
+ {/* Legend and time markers */}
+
+ {/* Legend */}
+
+
+
+
+ Active Work
+
+
+
+
+
+ Break Time
+
+
+ {isTimerRunning() && (
+
+
+
+ Live Session
+
+
+ )}
+
+
+ {/* Time markers */}
+
+
+ Started: {moment(timerData.startTime).format('HH:mm')}
+
+ {timerData.endTime && (
+
+ Ended: {moment(timerData.endTime).format('HH:mm')}
+
+ )}
+ {!timerData.endTime && (
+
+ Now: {moment(currentTime).format('HH:mm')}
+
+ )}
+
+ Active:{' '}
+ {calculateCurrentActiveDuration() > 0
+ ? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}%`
+ : '0%'}
+
+
+
+
+ ) : (
+
+
+ No activity timeline available. Start working to see your
+ activity pattern.
+
+
+ )}
+
{/* Session Breakdown */}
-
- Session Breakdown
-
+
+ Session Breakdown
+ {!editingSessions[timerData.id] && (
+ }
+ onClick={() => startEditingSession()}
+ size='sm'
+ >
+ Edit
+
+ )}
+ {editingSessions[timerData.id] && (
+
+
+
+
+ )}
+
{!editingSessions[timerData.id] ? (
@@ -656,93 +1094,233 @@ const TimerDetails = () => {
)
: pause.duration
+ const swipeState = getSessionSwipeState(pauseIndex)
+
return (
-
- {/* Session indicator */}
+ {/* Action buttons underneath (revealed on swipe) */}
-
- {/* Duration - Main focus */}
-
-
- {formatDuration(realTimeDuration)}
-
- {isOngoing && (
-
- Live
-
- )}
-
-
- {/* Session details */}
-
- {
+ e.stopPropagation()
+ handleEditSession()
+ }}
sx={{
- fontWeight: 'medium',
- color: 'text.secondary',
- mb: 0.2,
+ width: 40,
+ height: 40,
+ mx: 1,
}}
>
- Session #{pauseIndex + 1} • {sessionDate}
-
-
+
+
+ {
+ e.stopPropagation()
+ handleDeleteSession(pauseIndex)
+ }}
sx={{
- color: 'text.tertiary',
- fontFamily: 'monospace',
+ width: 40,
+ height: 40,
+ mx: 1,
}}
>
- {startTime}{' '}
- {endTime ? `→ ${endTime}` : '→ ongoing'}
-
+
+
-
+
+ {/* Session Card */}
+ {
+ if (swipeState.isRevealed) {
+ resetSessionSwipe(pauseIndex)
+ return
+ }
+ // Optional: Navigate to session details
+ }}
+ onTouchStart={handleSessionTouchStart}
+ onTouchMove={e =>
+ handleSessionTouchMove(e, pauseIndex)
+ }
+ onTouchEnd={e =>
+ handleSessionTouchEnd(e, pauseIndex)
+ }
+ onMouseDown={handleSessionMouseDown}
+ onMouseMove={e =>
+ handleSessionMouseMove(e, pauseIndex)
+ }
+ onMouseUp={e =>
+ handleSessionMouseUp(e, pauseIndex)
+ }
+ >
+ {/* Session indicator */}
+
+
+ {/* Duration - Main focus */}
+
+
+ {formatDuration(realTimeDuration)}
+
+ {isOngoing && (
+
+ Live
+
+ )}
+
+
+ {/* Session details */}
+
+
+ Session #{pauseIndex + 1} • {sessionDate}
+
+
+ {startTime}{' '}
+ {endTime ? `→ ${endTime}` : '→ ongoing'}
+
+
+
+ {/* Right drag indicator (desktop only) */}
+ {!isTouchDevice && (
+
+ {/* Drag indicator dots */}
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+ )}
+
+
)
})}
@@ -917,81 +1495,73 @@ const TimerDetails = () => {
)}
- {/* Sticky Bottom Actions */}
-
-
-
- {/* */}
-
- {/* Right side - Action buttons */}
- {!loading && timerData && !editingSessions[timerData.id] && (
-
-
- }
- onClick={() => startEditingSession()}
- >
- Edit
-
-
- )}
-
- {/* Save/Cancel buttons when editing */}
- {!loading && timerData && editingSessions[timerData.id] && (
-
-
-
-
- )}
-
-
-
-
-
+ {/* Floating Timer Control Button */}
+ {!loading && timerData && (
+
+ {isTimerRunning() ? (
+
+ ) : (
+
+ )}
+
+ )}
)
}
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index 864f757..a1fd787 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -159,7 +159,7 @@ const NavBar = () => {
sx={{
'& .MuiDrawer-content': {
position: 'fixed',
- top: 'calc(env(safe-area-inset-top, 0px) + 35px)',
+ top: 'calc(env(safe-area-inset-top, 0px) + 45px)',
left: 0,
height:
'calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))',
@@ -224,7 +224,7 @@ const NavBar = () => {
p: 1,
color: 'text.tertiary',
textAlign: 'center',
- bottom: 0,
+ mb: 'calc(env(safe-area-inset-bottom, 0px) + 45px)',
// mb: -2,
}}
>