diff --git a/src/App.jsx b/src/App.jsx
index 314097a..665fd18 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,13 +1,11 @@
import NavBar from '@/views/components/NavBar'
import { Button, Typography, useColorScheme } from '@mui/joy'
import Tracker from '@openreplay/tracker'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { useEffect } from 'react'
+import { useCallback, 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 {
NotificationProvider,
@@ -15,6 +13,7 @@ import {
} from './service/NotificationProvider'
import { apiManager } from './utils/TokenManager'
import NetworkBanner from './views/components/NetworkBanner'
+
const add = className => {
document.getElementById('root').classList.add(className)
}
@@ -22,9 +21,9 @@ const add = className => {
const remove = className => {
document.getElementById('root').classList.remove(className)
}
+
// TODO: Update the interval to at 60 minutes
const intervalMS = 5 * 60 * 1000 // 5 minutes
-const queryClient = new QueryClient({})
const AppContent = () => {
const { showNotification } = useNotification()
@@ -85,14 +84,13 @@ const AppContent = () => {
}
function App() {
- const resource = useResource()
const navigate = useNavigate()
startApiManager(navigate)
startOpenReplay()
const { mode, systemMode } = useColorScheme()
- const setThemeClass = () => {
+ const setThemeClass = useCallback(() => {
const value = JSON.parse(localStorage.getItem('themeMode')) || mode
if (value === 'system') {
@@ -107,11 +105,11 @@ function App() {
}
return remove('dark')
- }
+ }, [mode, systemMode])
useEffect(() => {
setThemeClass()
- }, [mode, systemMode])
+ }, [setThemeClass])
useEffect(() => {
registerCapacitorListeners()
@@ -120,13 +118,11 @@ function App() {
return (
)
}
@@ -139,7 +135,6 @@ const startOpenReplay = () => {
tracker.start()
}
-export default App
const startApiManager = navigate => {
apiManager.init()
@@ -147,3 +142,5 @@ const startApiManager = navigate => {
navigate('/login')
})
}
+
+export default App
diff --git a/src/main.jsx b/src/main.jsx
index 4e6fcdd..baf76fe 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,10 +1,16 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
+import { QueryClient } from '@tanstack/react-query'
+import App from './App.jsx'
import Contexts from './contexts/Contexts.jsx'
import './index.css'
+const queryClient = new QueryClient({})
+
ReactDOM.createRoot(document.getElementById('root')).render(
-
+
+
+
,
)
diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx
index bdf9c71..be7310f 100644
--- a/src/queries/ChoreQueries.jsx
+++ b/src/queries/ChoreQueries.jsx
@@ -178,7 +178,7 @@ export const useChoresHistory = (initialLimit, includeMembers) => {
export const useChoreDetails = choreId => {
return useQuery({
- queryKey: ['chore', choreId],
+ queryKey: ['choreDetails', choreId],
queryFn: async () => {
var onlineChore = null
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
index 52dda97..9292126 100644
--- a/src/views/ChoreEdit/ChoreView.jsx
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -527,6 +527,7 @@ const ChoreView = () => {
>
{
setChore({
diff --git a/src/views/ChoreEdit/TimePassedCard.jsx b/src/views/ChoreEdit/TimePassedCard.jsx
new file mode 100644
index 0000000..5799166
--- /dev/null
+++ b/src/views/ChoreEdit/TimePassedCard.jsx
@@ -0,0 +1,207 @@
+import { Flag, Schedule } from '@mui/icons-material'
+import { Box, Card, Chip, Typography } from '@mui/joy'
+import { useEffect, useRef, useState } from 'react'
+
+const TimePassedCard = ({ chore }) => {
+ const [time, setTime] = useState(0)
+ const [shouldAnimate, setShouldAnimate] = useState(false)
+ const [prevStatus, setPrevStatus] = useState(null) // Initialize as null
+ const intervalRef = useRef(null)
+
+ // Track status changes to trigger animation
+ useEffect(() => {
+ // Only trigger animation if we have a previous status and it changed from 0 to 1
+ if (prevStatus !== null && prevStatus === 0 && chore.status === 1) {
+ setShouldAnimate(true)
+ // Reset animation after it completes
+ const timer = setTimeout(() => setShouldAnimate(false), 300)
+ return () => clearTimeout(timer)
+ }
+ setPrevStatus(chore.status)
+ }, [chore.status, prevStatus])
+
+ // Single effect to handle both time calculation and timer
+ useEffect(() => {
+ // Calculate current time based on chore data
+ const calculateCurrentTime = () => {
+ if (chore.timerUpdatedAt && chore.status === 1) {
+ // Active session: base duration + time since start
+ return (
+ Math.floor(
+ (Date.now() - new Date(chore.timerUpdatedAt).getTime()) / 1000,
+ ) + (chore.duration || 0)
+ )
+ }
+ // Not active: just return accumulated duration
+ return chore.duration || 0
+ }
+
+ // Set initial time
+ const currentTime = calculateCurrentTime()
+ setTime(currentTime)
+
+ // Handle timer based on status
+ if (chore.status === 1) {
+ // Active: start interval timer
+ intervalRef.current = setInterval(() => {
+ setTime(calculateCurrentTime())
+ }, 1000)
+ } else {
+ // Not active: clear any existing timer
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ intervalRef.current = null
+ }
+ }
+
+ // Cleanup function
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ intervalRef.current = null
+ }
+ }
+ }, [chore.status, chore.timerUpdatedAt, chore.duration])
+
+ const formatTime = seconds => {
+ const hours = Math.floor(seconds / 3600)
+ const minutes = Math.floor((seconds % 3600) / 60)
+ const secs = seconds % 60
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
+ }
+
+ return (
+
+
+ {formatTime(time)}
+
+
+ {/* Status and info section */}
+
+ {/*
+ ) : chore.status === 2 ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {chore.status === 1
+ ? 'Active'
+ : chore.status === 2
+ ? 'Paused'
+ : 'Idle'}
+ */}
+
+ {/* Show start time and user if active */}
+ {chore.status === 1 && chore.timerUpdatedAt && (
+ <>
+ {/* Original start time */}
+ {chore.startTime && (
+ }
+ >
+ {'Started '}
+ {new Date(chore.startTime).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+ )}
+
+ {/* Current session start time */}
+ {chore.timerUpdatedAt !== chore.startTime && (
+ }
+ >
+ {'Session '}
+ {new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+ )}
+ >
+ )}
+
+ {/* Chips FOr paused : */}
+ {chore.status === 2 && (
+ <>
+ }
+ >
+ Paused
+
+ }
+ >
+ {new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+ >
+ )}
+
+
+ )
+}
+
+export default TimePassedCard
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index bf82031..345349f 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -51,15 +51,13 @@ import CompactChoreCard from './CompactChoreCard'
import IconButtonWithMenu from './IconButtonWithMenu'
import MultiSelectHelp from './MultiSelectHelp'
+import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { ChoreFilters, ChoresGrouper, ChoreSorter } from '../../utils/Chores'
import { DeleteChore, MarkChoreComplete, SkipChore } from '../../utils/Fetcher'
import TaskInput from '../components/AddTaskModal'
-import {
- canScheduleNotification,
- scheduleChoreNotification,
-} from './LocalNotificationScheduler'
+import { canScheduleNotification } from './LocalNotificationScheduler'
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
import Sidepanel from './Sidepanel'
import SortAndGrouping from './SortAndGrouping'
@@ -102,40 +100,50 @@ const MyChores = () => {
data: choresData,
isLoading: choresLoading,
refetch: refetchChores,
- } = useChores()
+ } = useChores(false)
const { data: membersData, isLoading: membersLoading } = useCircleMembers()
// Multi-select state
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
const [selectedChores, setSelectedChores] = useState(new Set())
const [confirmModelConfig, setConfirmModelConfig] = useState({})
-
+ const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
useEffect(() => {
- if (!choresLoading && !membersLoading && userProfile) {
- setPerformers(membersData.res)
- const sortedChores = choresData.res.sort(ChoreSorter)
- setChores(sortedChores)
- setFilteredChores(sortedChores)
- const sections = ChoresGrouper(
- selectedChoreSection,
- sortedChores,
- ChoreFilters(userProfile)[selectedChoreFilter],
- )
- setChoreSections(sections)
- if (localStorage.getItem('openChoreSections') === null) {
- setSelectedChoreSectionWithCache(selectedChoreSection)
- setOpenChoreSections(
- Object.keys(sections).reduce((acc, key) => {
- acc[key] = true
- return acc
- }, {}),
+ ;(async () => {
+ if (!choresLoading && !membersLoading && userProfile) {
+ setPerformers(membersData.res)
+ const sortedChores = choresData.res.sort(ChoreSorter)
+ setChores(sortedChores)
+ setFilteredChores(sortedChores)
+ const sections = ChoresGrouper(
+ selectedChoreSection,
+ sortedChores,
+ ChoreFilters(userProfile)[selectedChoreFilter],
+ )
+ setChoreSections(sections)
+ if (localStorage.getItem('openChoreSections') === null) {
+ setSelectedChoreSectionWithCache(selectedChoreSection)
+ setOpenChoreSections(
+ Object.keys(sections).reduce((acc, key) => {
+ acc[key] = true
+ return acc
+ }, {}),
+ )
+ }
+ console.log(
+ 'Checking if can schedule notification',
+ canScheduleNotification(),
)
- }
- if (canScheduleNotification()) {
- scheduleChoreNotification(choresData.res, userProfile, membersData.res)
+ if (await canScheduleNotification()) {
+ // scheduleChoreNotification(
+ // choresData.res,
+ // userProfile,
+ // membersData.res,
+ // )
+ }
}
- }
+ })()
}, [
membersLoading,
choresLoading,
@@ -164,6 +172,11 @@ const MyChores = () => {
// Keyboard shortcuts for multi-select and other actions
useEffect(() => {
const handleKeyDown = event => {
+ // if Ctrl/Cmd + / then show keyboard shortcuts modal
+ if (event.ctrlKey || event.metaKey) {
+ setShowKeyboardShortcuts(true)
+ }
+
// Ctrl/Cmd + K to open task modal
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault()
@@ -176,8 +189,13 @@ const MyChores = () => {
event.preventDefault()
searchInputRef.current?.focus()
return
+ // Ctrl/Cmd + X to close search input
+ } else if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
+ event.preventDefault()
+ if (searchTerm?.length > 0) {
+ handleSearchClose()
+ }
}
-
// Ctrl/Cmd + S Toggle Multi-select mode
else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault()
@@ -299,10 +317,17 @@ const MyChores = () => {
}
}
}
+ const handleKeyUp = event => {
+ if (!event.ctrlKey && !event.metaKey) {
+ setShowKeyboardShortcuts(false)
+ }
+ }
document.addEventListener('keydown', handleKeyDown)
+ document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
+ document.removeEventListener('keyup', handleKeyUp)
}
}, [isMultiSelectMode, selectedChores.size])
const setSelectedChoreSectionWithCache = value => {
@@ -506,7 +531,7 @@ const MyChores = () => {
const fuse = new Fuse(
chores.map(c => ({
...c,
- raw_label: c.labelsV2.map(c => c.name).join(' '),
+ raw_label: c.labelsV2?.map(c => c.name).join(' '),
})),
searchOptions,
)
@@ -526,6 +551,12 @@ const MyChores = () => {
setSearchTerm(term)
setFilteredChores(fuse.search(term).map(result => result.item))
}
+ const handleSearchClose = () => {
+ setSearchTerm('')
+ setFilteredChores(chores)
+ // remove the focus from the search input:
+ setSearchInputFocus(0)
+ }
// Multi-select helper functions
const toggleMultiSelectMode = () => {
@@ -870,15 +901,21 @@ const MyChores = () => {
padding: 1,
}}
onChange={handleSearchChange}
+ startDecorator={
+
+ }
endDecorator={
- searchTerm && (
- {
- setSearchTerm('')
- setFilteredChores(chores)
- }}
- />
- )
+
+ {searchTerm && (
+ <>
+
+
+ >
+ )}
+
}
/>
@@ -981,6 +1018,7 @@ const MyChores = () => {
>
{isMultiSelectMode ? : }
+
{/* Search Filter with animation */}
@@ -1202,6 +1240,12 @@ const MyChores = () => {
minWidth: 'auto',
'--Button-paddingInline': '0.75rem',
}}
+ endDecorator={
+ 0}
+ />
+ }
>
All
@@ -1220,6 +1264,13 @@ const MyChores = () => {
minWidth: 'auto',
'--Button-paddingInline': '0.75rem',
}}
+ endDecorator={
+ 0}
+ />
+ }
>
{selectedChores.size === 0 ? 'Close' : 'Clear'}
@@ -1252,6 +1303,12 @@ const MyChores = () => {
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
}}
+ endDecorator={
+ 0}
+ />
+ }
>
Complete
@@ -1265,6 +1322,12 @@ const MyChores = () => {
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
}}
+ endDecorator={
+ 0}
+ />
+ }
>
Skip
@@ -1278,6 +1341,12 @@ const MyChores = () => {
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
}}
+ endDecorator={
+ 0}
+ />
+ }
>
Archive
@@ -1292,6 +1361,13 @@ const MyChores = () => {
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
}}
+ endDecorator={
+ 0}
+ />
+ }
>
Delete
@@ -1473,6 +1549,12 @@ const MyChores = () => {
variant='outlined'
color='neutral'
startDecorator={}
+ endDecorator={
+
+ }
>
Show Archived
@@ -1550,6 +1632,12 @@ const MyChores = () => {
}}
/>
+
+
{addTaskModalOpen && (
diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx
index 5203850..53bb7b9 100644
--- a/src/views/History/HistoryCard.jsx
+++ b/src/views/History/HistoryCard.jsx
@@ -1,59 +1,82 @@
-import { CalendarViewDay, Check, Timelapse } from '@mui/icons-material'
+import {
+ AccessTime,
+ Assignment,
+ CalendarViewDay,
+ Check,
+ EventNote,
+ Person,
+ Timelapse,
+} from '@mui/icons-material'
import {
Avatar,
Box,
Chip,
+ Grid,
ListDivider,
ListItem,
ListItemContent,
- ListItemDecorator,
Typography,
} from '@mui/joy'
import moment from 'moment'
-export const getCompletedChip = historyEntry => {
- var text = 'No Due Date'
- var color = 'info'
- var icon =
- // if completed few hours +-6 hours
- if (
- historyEntry.dueDate &&
- historyEntry.performedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 &&
- historyEntry.performedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6
- ) {
- text = 'On Time'
- color = 'success'
- icon =
- } else if (
- historyEntry.dueDate &&
- historyEntry.performedAt < historyEntry.dueDate
- ) {
- text = 'On Time'
- color = 'success'
- icon =
+/**
+ * Enhanced completion status chip with better logic and visual design
+ */
+const getCompletedChip = historyEntry => {
+ if (historyEntry.status === 0) {
+ return null
+ }
+ if (!historyEntry.dueDate) {
+ return (
+ }
+ >
+ No Due Date
+
+ )
}
- // if completed after due date then it's late
- else if (
- historyEntry.dueDate &&
- historyEntry.performedAt > historyEntry.dueDate
- ) {
- text = 'Late'
- color = 'warning'
- icon =
+ const performedAt = moment(historyEntry.performedAt)
+ const dueDate = moment(historyEntry.dueDate)
+ const gracePeriod = 6 * 60 * 60 * 1000 // 6 hours in milliseconds
+
+ if (Math.abs(performedAt - dueDate) <= gracePeriod) {
+ return (
+ }
+ >
+ On Time
+
+ )
+ } else if (performedAt.isBefore(dueDate)) {
+ return (
+ }>
+ Early
+
+ )
} else {
- text = 'No Due Date'
- color = 'neutral'
- icon =
+ return (
+ }
+ >
+ Late
+
+ )
}
-
- return (
-
- {text}
-
- )
}
+/**
+ * Compact HistoryCard component with improved UX and 2-row height design
+ */
const HistoryCard = ({
allHistory,
performers,
@@ -61,7 +84,10 @@ const HistoryCard = ({
index,
onClick,
}) => {
- function formatTimeDifference(startDate, endDate) {
+ const performer = performers.find(p => p.userId === historyEntry.completedBy)
+ const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo)
+
+ const formatTimeDifference = (startDate, endDate) => {
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
let timeValue = diffInMinutes
let unit = 'minute'
@@ -81,86 +107,187 @@ const HistoryCard = ({
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
}
+ const getStatusAvatar = () => {
+ const statusMap = {
+ 0: { icon: , color: 'primary' }, // Started
+ 1: { icon: , color: 'success' }, // Completed
+ 2: { icon: , color: 'danger' }, // Skipped
+ }
+
+ const config = statusMap[historyEntry.status] || statusMap[1]
+ return (
+
+ {config.icon}
+
+ )
+ }
+
return (
<>
-
- {' '}
- {/* Adjusted spacing and alignment */}
-
-
- {performers
- .find(p => p.userId === historyEntry.completedBy)
- ?.displayName?.charAt(0) || '?'}
-
-
-
- {' '}
- {/* Removed vertical margin */}
-
-
- {historyEntry.performedAt
- ? moment(historyEntry.performedAt).format(
- 'ddd MM/DD/yyyy HH:mm',
- )
- : 'Skipped'}
-
- {getCompletedChip(historyEntry)}
-
-
-
- {
- performers.find(p => p.userId === historyEntry.completedBy)
- ?.displayName
+ {' '}
- completed
- {historyEntry.completedBy !== historyEntry.assignedTo && (
- <>
- {', '}
- assigned to{' '}
-
- {
- performers.find(p => p.userId === historyEntry.assignedTo)
- ?.displayName
- }
+ : {},
+ borderRadius: 'sm',
+ transition: 'background-color 0.2s',
+ }}
+ >
+
+
+ {/* First Row/Column: Status and Time Info */}
+
+
+ {getStatusAvatar()}
+
+
+ {historyEntry.status === 0
+ ? 'In Progress'
+ : historyEntry.status === 1
+ ? 'Completed'
+ : 'Skipped'}
+
+
+
+ {moment(
+ historyEntry.performedAt || historyEntry.updatedAt,
+ ).format('MMM DD, h:mm A')}
+
+
+
+ {getCompletedChip(historyEntry)}
+
+
+
+
+ {/* Second Row/Column: Completion Status (right side on desktop) */}
+
+
+ {historyEntry.dueDate && (
+
+ Due: {moment(historyEntry.dueDate).format('MMM DD')}
+
+ )}
+
+
+
+ {/* Third Row: Performer and Assignment Info */}
+
+
+ }>
+ {performer?.displayName || 'Unknown'}
- >
- )}
-
- {historyEntry.dueDate && (
-
- Due: {moment(historyEntry.dueDate).format('ddd MM/DD/yyyy')}
-
- )}
- {historyEntry.notes && (
-
- Note: {historyEntry.notes}
-
- )}
+
+ {historyEntry.completedBy !== historyEntry.assignedTo &&
+ assignedTo && (
+ <>
+
+ →
+
+ }
+ >
+ {assignedTo.displayName}
+
+ >
+ )}
+
+ {historyEntry.notes && (
+ }
+ sx={{ maxWidth: '120px', overflow: 'hidden' }}
+ >
+ Note
+
+ )}
+
+
+
- {index < allHistory.length - 1 && (
- <>
-
- {/* time between two completion: */}
- {index < allHistory.length - 1 &&
- allHistory[index + 1].performedAt && (
-
- {formatTimeDifference(
- historyEntry.performedAt,
- allHistory[index + 1].performedAt,
- )}{' '}
- before
-
- )}
-
- >
+
+ {/* Compact Divider with Time Difference */}
+ {index < allHistory.length - 1 && allHistory[index + 1].performedAt && (
+
+
+ {formatTimeDifference(
+ historyEntry.performedAt || historyEntry.updatedAt,
+ allHistory[index + 1].performedAt,
+ )}{' '}
+ before
+
+
)}
>
)
diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx
index 81c9b47..10d8278 100644
--- a/src/views/Labels/LabelView.jsx
+++ b/src/views/Labels/LabelView.jsx
@@ -1,25 +1,375 @@
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import {
+ Avatar,
Box,
- Button,
Chip,
CircularProgress,
Container,
IconButton,
Typography,
} from '@mui/joy'
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import LabelModal from '../Modals/Inputs/LabelModal'
// import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Add } from '@mui/icons-material'
import { useQueryClient } from '@tanstack/react-query'
import { getTextColorFromBackgroundColor } from '../../utils/Colors'
+import LABEL_COLORS from '../../utils/Colors'
import { DeleteLabel } from '../../utils/Fetcher'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import { useLabels } from './LabelQueries'
+const LabelCard = ({ label, onEditClick, onDeleteClick }) => {
+ // Helper function to get color name from hex value
+ const getColorName = hexValue => {
+ const colorObj = LABEL_COLORS.find(
+ color => color.value.toLowerCase() === hexValue.toLowerCase(),
+ )
+ return colorObj ? colorObj.name : hexValue
+ }
+
+ // Swipe functionality state
+ const [swipeTranslateX, setSwipeTranslateX] = useState(0)
+ const [isDragging, setIsDragging] = useState(false)
+ const [isSwipeRevealed, setIsSwipeRevealed] = useState(false)
+ const [hoverTimer, setHoverTimer] = useState(null)
+ const swipeThreshold = 80
+ const maxSwipeDistance = 160
+ const dragStartX = useRef(0)
+ const cardRef = useRef(null)
+
+ // Swipe gesture handlers
+ const handleTouchStart = e => {
+ dragStartX.current = e.touches[0].clientX
+ setIsDragging(true)
+ }
+
+ const handleTouchMove = e => {
+ if (!isDragging) return
+
+ const currentX = e.touches[0].clientX
+ const deltaX = currentX - dragStartX.current
+
+ if (isSwipeRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ setSwipeTranslateX(clampedDelta)
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ setSwipeTranslateX(clampedDelta)
+ }
+ }
+ }
+
+ const handleTouchEnd = () => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ if (isSwipeRevealed) {
+ if (swipeTranslateX > -swipeThreshold) {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ } else {
+ setSwipeTranslateX(-maxSwipeDistance)
+ }
+ } else {
+ if (Math.abs(swipeTranslateX) > swipeThreshold) {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ } else {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+ }
+ }
+
+ const handleMouseDown = e => {
+ dragStartX.current = e.clientX
+ setIsDragging(true)
+ }
+
+ const handleMouseMove = e => {
+ if (!isDragging) return
+
+ const currentX = e.clientX
+ const deltaX = currentX - dragStartX.current
+
+ if (isSwipeRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ setSwipeTranslateX(clampedDelta)
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ setSwipeTranslateX(clampedDelta)
+ }
+ }
+ }
+
+ const handleMouseUp = () => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ if (isSwipeRevealed) {
+ if (swipeTranslateX > -swipeThreshold) {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ } else {
+ setSwipeTranslateX(-maxSwipeDistance)
+ }
+ } else {
+ if (Math.abs(swipeTranslateX) > swipeThreshold) {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ } else {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+ }
+ }
+
+ const resetSwipe = () => {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+
+ // Hover functionality for desktop
+ const handleMouseEnter = () => {
+ if (isSwipeRevealed) return
+ const timer = setTimeout(() => {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ setHoverTimer(null)
+ }, 1500)
+ setHoverTimer(timer)
+ }
+
+ const handleMouseLeave = () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ if (isSwipeRevealed) {
+ resetSwipe()
+ }
+ }
+
+ const handleActionAreaMouseEnter = () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ }
+
+ // Clean up timer on unmount
+ useEffect(() => {
+ return () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ }
+ }
+ }, [hoverTimer])
+
+ return (
+
+
+ {/* Action buttons underneath (revealed on swipe) */}
+
+ {
+ e.stopPropagation()
+ resetSwipe()
+ onEditClick(label)
+ }}
+ sx={{
+ width: 40,
+ height: 40,
+ mx: 1,
+ bgcolor: 'primary.100',
+ color: 'primary.600',
+ '&:hover': {
+ bgcolor: 'primary.200',
+ },
+ }}
+ >
+
+
+
+ {
+ e.stopPropagation()
+ resetSwipe()
+ onDeleteClick(label.id)
+ }}
+ sx={{
+ width: 40,
+ height: 40,
+ mx: 1,
+ bgcolor: 'danger.100',
+ color: 'danger.600',
+ '&:hover': {
+ bgcolor: 'danger.200',
+ },
+ }}
+ >
+
+
+
+
+ {/* Main card content */}
+ {
+ if (isSwipeRevealed) {
+ resetSwipe()
+ return
+ }
+ // Optional: Navigate to label details or edit directly
+ onEditClick(label)
+ }}
+ onTouchStart={handleTouchStart}
+ onTouchMove={handleTouchMove}
+ onTouchEnd={handleTouchEnd}
+ onMouseDown={handleMouseDown}
+ onMouseMove={handleMouseMove}
+ onMouseUp={handleMouseUp}
+ onMouseEnter={handleMouseEnter}
+ >
+ {/* Color Avatar */}
+
+
+
+ {label.name.charAt(0).toUpperCase()}
+
+
+
+
+ {/* Content - Center */}
+
+ {/* Label Name */}
+
+ {label.name}
+
+
+ {/* Color Info */}
+
+
+ {getColorName(label.color)}
+
+
+
+
+
+
+ )
+}
+
const LabelView = () => {
const { data: labels, isLabelsLoading, isError } = useLabels()
@@ -61,7 +411,7 @@ const LabelView = () => {
}
const handleDeleteLabel = id => {
- DeleteLabel(id).then(res => {
+ DeleteLabel(id).then(() => {
const updatedLabels = userLabels.filter(label => label.id !== id)
setUserLabels(updatedLabels)
@@ -106,54 +456,40 @@ const LabelView = () => {
}
return (
-
-
- {userLabels.map(label => (
-
+
+ {userLabels.length === 0 && (
+
-
- {label.name}
-
-
-
-
- handleDeleteClicked(label.id)}
- color='danger'
- >
-
-
-
-
+
+ No labels available. Add a new label to get started.
+
+
+ )}
+ {userLabels.map(label => (
+
))}
-
-
- {userLabels.length === 0 && (
-
- No labels available. Add a new label to get started.
-
- )}
+
{modalOpen && (
{
const [isDisabled, setIsDisabled] = useState(false)
const Navigate = useNavigate()
+
const getThingIcon = type => {
if (type === 'text') {
return
@@ -54,6 +58,43 @@ const ThingCard = ({
}
}
+ const getThingAvatar = () => {
+ const typeConfig = {
+ text: { color: 'primary', icon: },
+ number: { color: 'success', icon: },
+ boolean: {
+ color: thing.state === 'true' ? 'success' : 'neutral',
+ icon: thing.state === 'true' ? :
+ },
+ }
+
+ const config = typeConfig[thing?.type] || typeConfig.boolean
+ return (
+
+ {config.icon}
+
+ )
+ }
+
+ const getActionButtonProps = () => {
+ const buttonConfig = {
+ text: { text: 'Change', color: 'primary' },
+ number: { text: 'Increment', color: 'success' },
+ boolean: { text: 'Toggle', color: 'warning' },
+ }
+
+ return buttonConfig[thing?.type] || buttonConfig.boolean
+ }
+
const handleRequestChange = thing => {
setIsDisabled(true)
onStateChangeRequest(thing)
@@ -62,103 +103,158 @@ const ThingCard = ({
}, 2000)
}
- return (
- Navigate(`/things/${thing?.id}`)}
>
-
- Navigate(`/things/${thing?.id}`)}
- >
+
+ {/* First Row: Thing Info */}
+
+ {getThingAvatar()}
+
+
+
+ {thing?.name}
+
+
+
+
+ {thing?.type}
+
+
+
+ •
+
+
+
+ Current state:
+
+
+
+ {thing?.state}
+
+
+
+
+
+
+ {/* Second Row: Action Buttons */}
+
+ Navigate(`/things/${thing?.id}`)}
+ onClick={(e) => e.stopPropagation()}
>
- {thing?.name}
- {
+ if (thing?.type === 'text') {
+ onEditClick(thing)
+ } else {
+ handleRequestChange(thing)
+ }
+ }}
+ disabled={isDisabled}
+ startDecorator={getThingIcon(thing?.type)}
+ sx={{
+ minWidth: '80px',
+ fontWeight: 'md',
}}
>
- {thing?.type}
-
-
- State: {thing?.state}
-
-
-
+
+ {
+ e.stopPropagation()
onEditClick(thing)
- } else {
- handleRequestChange(thing)
- }
- }}
- disabled={isDisabled}
- startDecorator={getThingIcon(thing?.type)}
- >
- {thing?.type === 'text'
- ? 'Change'
- : thing?.type === 'number'
- ? 'Increment'
- : 'Toggle'}
-
- onEditClick(thing)}
- sx={{
- borderRadius: '50%',
- width: 30,
- height: 30,
- ml: 1,
- transition: 'background-color 0.2s',
- '&:hover': { backgroundColor: 'action.hover' },
- }}
- >
-
-
- onDeleteClick(thing)}
- sx={{
- borderRadius: '50%',
- width: 30,
- height: 30,
- ml: 1,
- }}
- >
-
-
+ }}
+ sx={{
+ borderRadius: '50%',
+ width: 32,
+ height: 32,
+ transition: 'all 0.2s',
+ '&:hover': {
+ backgroundColor: 'primary.softBg',
+ borderColor: 'primary.300',
+ },
+ }}
+ >
+
+
+
+ {
+ e.stopPropagation()
+ onDeleteClick(thing)
+ }}
+ sx={{
+ borderRadius: '50%',
+ width: 32,
+ height: 32,
+ transition: 'all 0.2s',
+ '&:hover': {
+ backgroundColor: 'danger.softBg',
+ borderColor: 'danger.300',
+ },
+ }}
+ >
+
+
+
-
+
)
}
diff --git a/src/views/components/SubTask.jsx b/src/views/components/SubTask.jsx
index ae214bf..9bf13ec 100644
--- a/src/views/components/SubTask.jsx
+++ b/src/views/components/SubTask.jsx
@@ -24,6 +24,7 @@ import {
import {
Box,
Checkbox,
+ Chip,
IconButton,
Input,
List,
@@ -31,6 +32,7 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
+import { useUserProfile } from '../../queries/UserQueries'
import { CompleteSubTask } from '../../utils/Fetcher'
function SortableItem({
@@ -43,10 +45,12 @@ function SortableItem({
setTasks,
level = 0,
editMode,
+ performers = [],
}) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({
id: task.id,
+ data: { completedAt: task.completedAt, completedBy: task.completedBy },
// Add touch sensor options for better mobile scrolling
options: {
activationConstraint: {
@@ -206,6 +210,14 @@ function SortableItem({
}}
>
{new Date(task.completedAt).toLocaleString()}
+ {performers.find(p => p.userId === task.completedBy) ? (
+
+ {
+ performers.find(p => p.userId === task.completedBy)
+ .displayName
+ }
+
+ ) : null}
)}
@@ -281,6 +293,7 @@ function SortableItem({
setTasks={setTasks}
level={level + 1}
editMode={editMode}
+ performers={performers}
/>
))}
@@ -289,8 +302,15 @@ function SortableItem({
)
}
-const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
+const SubTasks = ({
+ editMode = true,
+ choreId = 0,
+ tasks = [],
+ setTasks,
+ performers,
+}) => {
const [newTask, setNewTask] = useState('')
+ const { data: userProfile } = useUserProfile()
const topLevelTasks = tasks.filter(task => task.parentId === null)
@@ -313,7 +333,13 @@ const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
// Update the task
const updatedTasks = tasks.map(task =>
- task.id === taskId ? { ...task, completedAt: newCompletedAt } : task,
+ task.id === taskId
+ ? {
+ ...task,
+ completedAt: newCompletedAt,
+ completedBy: userProfile?.id,
+ }
+ : task,
)
// If completing a task, also complete all child tasks
@@ -469,6 +495,7 @@ const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
allTasks={tasks}
setTasks={setTasks}
editMode={editMode}
+ performers={performers}
/>
))}
{editMode && (