From 373096b339b6a2dbdd88a77b5b804e6ff78ac780 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sat, 7 Feb 2026 23:26:06 -0500 Subject: [PATCH] Rewrite the Swiping Logic and update the Project, Labelm Things,Chore and all swipe card/view --- package.json | 1 + src/views/Chores/ArchivedTasks.jsx | 13 +- src/views/Chores/ChoreCard.jsx | 423 +--------- src/views/Chores/ChoreListView.jsx | 270 ++++++ src/views/Chores/CompactChoreCard.jsx | 1127 ++++++++----------------- src/views/Chores/MyChores.jsx | 125 ++- src/views/Filters/FilterView.jsx | 709 +++++----------- src/views/History/ChoreHistory.jsx | 102 ++- src/views/History/HistoryCard.jsx | 622 +++----------- src/views/Labels/LabelView.jsx | 546 ++++-------- src/views/Projects/ProjectView.jsx | 695 +++++---------- src/views/Things/ThingsView.jsx | 630 +++++--------- src/views/Timer/TimerDetails.jsx | 397 ++------- 13 files changed, 1816 insertions(+), 3844 deletions(-) create mode 100644 src/views/Chores/ChoreListView.jsx diff --git a/package.json b/package.json index 6dcf723..d18eee6 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "react-dom": "^18.2.0", "react-easy-crop": "^5.4.2", "react-router-dom": "^6.21.1", + "react-swipeable-list": "^1.10.0", "react-transition-group": "^4.4.5", "reactjs-social-login": "^2.6.3", "recharts": "^2.15.0", diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx index 6d4eee6..4b3daf1 100644 --- a/src/views/Chores/ArchivedTasks.jsx +++ b/src/views/Chores/ArchivedTasks.jsx @@ -32,6 +32,7 @@ import { DeleteChore, GetArchivedChores } from '../../utils/Fetcher' import LoadingComponent from '../components/Loading' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import ChoreCard from './ChoreCard' +import ChoreListView from './ChoreListView.jsx' import CompactChoreCard from './CompactChoreCard' import MultiSelectHelp from './MultiSelectHelp' @@ -851,9 +852,15 @@ const ArchivedTasks = () => { - {filteredChores.map(chore => - renderChoreCard(chore, `archived-${chore.id}`), - )} + )} diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index 22267ed..0da767c 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -1,15 +1,10 @@ import { Check, - Delete, - Edit, Group, HourglassEmpty, - Notifications, Pause, PlayArrow, Repeat, - Schedule, - ThumbDown, ThumbUp, TimesOneMobiledata, Toll, @@ -26,13 +21,10 @@ import { Typography, } from '@mui/joy' import moment from 'moment' -import React from 'react' -import { useNavigate } from 'react-router-dom' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useUserProfile } from '../../queries/UserQueries.jsx' import { notInCompletionWindow } from '../../utils/Chores.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' -import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle' import Priorities from '../../utils/Priorities' import ChoreActionMenu from '../components/ChoreActionMenu' const ChoreCard = ({ @@ -48,40 +40,10 @@ const ChoreCard = ({ isSelected = false, onSelectionToggle, }) => { - const [isOfficialInstance, setIsOfficialInstance] = React.useState(false) - const navigate = useNavigate() - const { data: userProfile } = useUserProfile() const { impersonatedUser } = useImpersonateUser() - // Swipe functionality state - const [swipeTranslateX, setSwipeTranslateX] = React.useState(0) - const [isDragging, setIsDragging] = React.useState(false) - const [isSwipeRevealed, setIsSwipeRevealed] = React.useState(false) - const [hoverTimer, setHoverTimer] = React.useState(null) - const [isTouchDevice, setIsTouchDevice] = React.useState(false) - const swipeThreshold = 80 // Minimum swipe distance to reveal actions - const maxSwipeDistance = 260 // Maximum swipe distance - const dragStartX = React.useRef(0) - const cardRef = React.useRef(null) - - // Detect if device supports touch - React.useEffect(() => { - const checkTouchDevice = () => { - setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0) - } - checkTouchDevice() - - // Check if this is the official donetick.com instance - try { - setIsOfficialInstance(isOfficialDonetickInstanceSync()) - } catch (error) { - console.warn('Error checking instance type:', error) - setIsOfficialInstance(false) - } - }, []) - // Check if the current user can approve/reject (admin, manager, or task owner) const canApproveReject = () => { if (!performers || !chore) return false @@ -100,177 +62,6 @@ const ChoreCard = ({ ) } - // Swipe gesture handlers - const handleTouchStart = e => { - if (isMultiSelectMode || viewOnly) return - - dragStartX.current = e.touches[0].clientX - setIsDragging(true) - } - - const handleTouchMove = e => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - const currentX = e.touches[0].clientX - const deltaX = currentX - dragStartX.current - - if (isSwipeRevealed) { - // When actions are revealed, allow right swipe to hide - if (deltaX > 0) { - const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0) - setSwipeTranslateX(clampedDelta) - } - } else { - // When actions are hidden, allow left swipe to reveal - if (deltaX < 0) { - const clampedDelta = Math.max(deltaX, -maxSwipeDistance) - setSwipeTranslateX(clampedDelta) - } - } - } - - const handleTouchEnd = () => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - setIsDragging(false) - - if (isSwipeRevealed) { - // When actions are revealed, check if user swiped right enough to hide - if (swipeTranslateX > -swipeThreshold) { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } else { - // Snap back to revealed position - setSwipeTranslateX(-maxSwipeDistance) - } - } else { - // When actions are hidden, check if user swiped left enough to reveal - if (Math.abs(swipeTranslateX) > swipeThreshold) { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - } else { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } - } - } - - const handleMouseDown = e => { - if (isMultiSelectMode || viewOnly) return - - dragStartX.current = e.clientX - setIsDragging(true) - } - - const handleMouseMove = e => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - const currentX = e.clientX - const deltaX = currentX - dragStartX.current - - if (isSwipeRevealed) { - // When actions are revealed, allow right swipe to hide - if (deltaX > 0) { - const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0) - setSwipeTranslateX(clampedDelta) - } - } else { - // When actions are hidden, allow left swipe to reveal - if (deltaX < 0) { - const clampedDelta = Math.max(deltaX, -maxSwipeDistance) - setSwipeTranslateX(clampedDelta) - } - } - } - - const handleMouseUp = () => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - setIsDragging(false) - - if (isSwipeRevealed) { - // When actions are revealed, check if user swiped right enough to hide - if (swipeTranslateX > -swipeThreshold) { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } else { - // Snap back to revealed position - setSwipeTranslateX(-maxSwipeDistance) - } - } else { - // When actions are hidden, check if user swiped left enough to reveal - if (Math.abs(swipeTranslateX) > swipeThreshold) { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - } else { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } - } - } - - const resetSwipe = () => { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } - - // Hover functionality for desktop - only trigger from action menu - const handleMouseEnter = () => { - if (isMultiSelectMode || viewOnly || isSwipeRevealed || isTouchDevice) - return - const timer = setTimeout(() => { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - setHoverTimer(null) - }, 1500) // Match CompactChoreCard delay - setHoverTimer(timer) - } - - const handleMouseLeave = () => { - if (isTouchDevice) return - - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - - // Add a small delay before hiding to allow moving to action area - if (isSwipeRevealed) { - const hideTimer = setTimeout(() => { - resetSwipe() - }, 300) // Match CompactChoreCard delay - setHoverTimer(hideTimer) - } - } - - const handleActionAreaMouseEnter = () => { - if (isTouchDevice) return - - // Clear any pending timer when entering action area (both show and hide timers) - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - } - - const handleActionAreaMouseLeave = () => { - if (isTouchDevice) return - - // Hide immediately when leaving action area (like CompactChoreCard) - if (isSwipeRevealed) { - resetSwipe() - } - } - - // Clean up timer on unmount - React.useEffect(() => { - return () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - } - } - }, [hoverTimer]) - const getDueDateChipText = nextDueDate => { if (chore.nextDueDate === null) return 'No Due Date' // if due in next 48 hours, we should it in this format : Tomorrow 11:00 AM @@ -416,7 +207,7 @@ const ChoreCard = ({ return name } return ( - + - {/* Action buttons underneath (revealed on swipe) */} - - {chore.status === 3 ? ( - // Pending approval: Show approve/reject for admins/managers/owners - canApproveReject() ? ( - <> - {/* { - e.stopPropagation() - resetSwipe() - onAction('approve', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - */} - { - e.stopPropagation() - resetSwipe() - onAction('reject', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - ) : ( - - - - ) - ) : ( - { - e.stopPropagation() - resetSwipe() - - if (chore.status !== 0) { - onAction('complete', chore) - } else { - onAction('start', chore) - } - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - {chore.status !== 0 ? ( - - ) : ( - - )} - - )} - - { - e.stopPropagation() - resetSwipe() - onAction('changeDueDate', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - { - e.stopPropagation() - resetSwipe() - navigate(`/chores/${chore.id}/edit`) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - {isOfficialInstance && ( - { - e.stopPropagation() - resetSwipe() - onAction('nudge', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - )} - - { - e.stopPropagation() - resetSwipe() - onAction('delete', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - {/* Multi-select checkbox */} {isMultiSelectMode && ( @@ -714,13 +317,13 @@ const ChoreCard = ({ { - if (isMultiSelectMode) { - onSelectionToggle() - } else { - navigate(`/chores/${chore.id}`) - } - }} + // onClick={() => { + // if (isMultiSelectMode) { + // onSelectionToggle() + // } else { + // navigate(`/chores/${chore.id}`) + // } + // }} > {/* Box in top right with Chip showing next due date */} @@ -1007,14 +610,6 @@ const ChoreCard = ({ onWriteNFC={() => onAction('writeNFC', chore)} onNudge={() => onAction('nudge', chore)} onDelete={() => onAction('delete', chore)} - onMouseEnter={handleMouseEnter} - onOpen={() => { - // Clear any pending hide timer when menu opens - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - }} /> )} diff --git a/src/views/Chores/ChoreListView.jsx b/src/views/Chores/ChoreListView.jsx new file mode 100644 index 0000000..bdfdebf --- /dev/null +++ b/src/views/Chores/ChoreListView.jsx @@ -0,0 +1,270 @@ +import { + Check, + Delete, + Edit, + HourglassEmpty, + Notifications, + PlayArrow, + Schedule, + ThumbDown, +} from '@mui/icons-material' +import { Box, Typography } from '@mui/joy' +import { useNavigate } from 'react-router-dom' +import { + Type as ListType, + SwipeableList, + SwipeableListItem, + SwipeAction, + TrailingActions, +} from 'react-swipeable-list' +import 'react-swipeable-list/dist/styles.css' +import ChoreCard from './ChoreCard' +import CompactChoreCard from './CompactChoreCard' + +const ChoreListView = ({ + chores, + viewMode, + membersData, + userLabels, + handleLabelFiltering, + handleChoreAction, + isMultiSelectMode, + selectedChores, + toggleChoreSelection, + userProfile, + isOfficialInstance, + toggleMultiSelectMode, + showActions = true, +}) => { + const navigate = useNavigate() + const renderChoreCard = (chore, key) => { + const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard + return ( + toggleChoreSelection(chore.id)} + showActions={showActions} + /> + ) + } + const canApproveReject = chore => { + return userProfile?.role === 1 || chore.createdBy === userProfile?.id + } + + const getTrailingActions = chore => { + if (isMultiSelectMode) return null + if (!showActions) return null + + const isCompact = viewMode === 'compact' + + return ( + + + {chore.status === 3 ? ( + canApproveReject(chore) ? ( + handleChoreAction('reject', chore)}> + + + + Reject + + + + ) : ( + {}}> + + + + Pending + + + + ) + ) : ( + { + if (chore.status === 0 || chore.status === 2) { + handleChoreAction('start', chore) + } else { + handleChoreAction('complete', chore) + } + }} + > + + {chore.status !== 1 ? ( + + ) : ( + + )} + + {chore.status !== 1 ? 'Start' : 'Complete'} + + + + )} + + handleChoreAction('changeDueDate', chore)} + > + + + + Schedule + + + + + navigate(`/chores/${chore.id}/edit`)}> + + + + Edit + + + + + {isOfficialInstance && ( + handleChoreAction('nudge', chore)}> + + + + Nudge + + + + )} + + handleChoreAction('delete', chore)}> + + + + Delete + + + + + + ) + } + + const renderChores = chores => { + return ( + + {chores.map(chore => ( + { + if (isMultiSelectMode) { + toggleChoreSelection(chore.id) + } else { + navigate(`/chores/${chore.id}`) + } + }} + > + {renderChoreCard(chore)} + + ))} + + ) + } + + return <>{renderChores(chores)} +} + +export default ChoreListView diff --git a/src/views/Chores/CompactChoreCard.jsx b/src/views/Chores/CompactChoreCard.jsx index a9b184c..05bfe96 100644 --- a/src/views/Chores/CompactChoreCard.jsx +++ b/src/views/Chores/CompactChoreCard.jsx @@ -1,21 +1,15 @@ import { Check, - Delete, - Edit, HourglassEmpty, - Notifications, Pause, PlayArrow, Repeat, - Schedule, - ThumbDown, ThumbUp, TimesOneMobiledata, Webhook, } from '@mui/icons-material' import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy' import moment from 'moment' -import React from 'react' import { useNavigate } from 'react-router-dom' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx' @@ -24,7 +18,6 @@ import { getPriorityColor, getTextColorFromBackgroundColor, } from '../../utils/Colors.jsx' -import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle' import ChoreActionMenu from '../components/ChoreActionMenu' const CompactChoreCard = ({ @@ -39,8 +32,8 @@ const CompactChoreCard = ({ isMultiSelectMode = false, isSelected = false, onSelectionToggle, + onlyClickable = false, }) => { - const [isOfficialInstance, setIsOfficialInstance] = React.useState(false) const navigate = useNavigate() const { data: userProfile } = useUserProfile() @@ -48,204 +41,6 @@ const CompactChoreCard = ({ const { impersonatedUser } = useImpersonateUser() - // Swipe functionality state - const [swipeTranslateX, setSwipeTranslateX] = React.useState(0) - const [isDragging, setIsDragging] = React.useState(false) - const [isSwipeRevealed, setIsSwipeRevealed] = React.useState(false) - const [hoverTimer, setHoverTimer] = React.useState(null) - const [isTouchDevice, setIsTouchDevice] = React.useState(false) - const swipeThreshold = 80 // Minimum swipe distance to reveal actions - const maxSwipeDistance = 260 // Maximum swipe distance - const dragStartX = React.useRef(0) - const cardRef = React.useRef(null) - - // Detect if device supports touch - React.useEffect(() => { - const checkTouchDevice = () => { - setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0) - } - checkTouchDevice() - - // Check if this is the official donetick.com instance - try { - setIsOfficialInstance(isOfficialDonetickInstanceSync()) - } catch (error) { - console.warn('Error checking instance type:', error) - setIsOfficialInstance(false) - } - }, []) - - // Swipe gesture handlers - const handleTouchStart = e => { - if (isMultiSelectMode || viewOnly) return - - dragStartX.current = e.touches[0].clientX - setIsDragging(true) - } - - const handleTouchMove = e => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - const currentX = e.touches[0].clientX - const deltaX = currentX - dragStartX.current - - if (isSwipeRevealed) { - // When actions are revealed, allow right swipe to hide - if (deltaX > 0) { - const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0) - setSwipeTranslateX(clampedDelta) - } - } else { - // When actions are hidden, allow left swipe to reveal - if (deltaX < 0) { - const clampedDelta = Math.max(deltaX, -maxSwipeDistance) - setSwipeTranslateX(clampedDelta) - } - } - } - - const handleTouchEnd = () => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - setIsDragging(false) - - if (isSwipeRevealed) { - // When actions are revealed, check if user swiped right enough to hide - if (swipeTranslateX > -swipeThreshold) { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } else { - // Snap back to revealed position - setSwipeTranslateX(-maxSwipeDistance) - } - } else { - // When actions are hidden, check if user swiped left enough to reveal - if (Math.abs(swipeTranslateX) > swipeThreshold) { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - } else { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } - } - } - - const handleMouseDown = e => { - if (isMultiSelectMode || viewOnly) return - - dragStartX.current = e.clientX - setIsDragging(true) - } - - const handleMouseMove = e => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - const currentX = e.clientX - const deltaX = currentX - dragStartX.current - - if (isSwipeRevealed) { - // When actions are revealed, allow right swipe to hide - if (deltaX > 0) { - const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0) - setSwipeTranslateX(clampedDelta) - } - } else { - // When actions are hidden, allow left swipe to reveal - if (deltaX < 0) { - const clampedDelta = Math.max(deltaX, -maxSwipeDistance) - setSwipeTranslateX(clampedDelta) - } - } - } - - const handleMouseUp = () => { - if (isMultiSelectMode || viewOnly || !isDragging) return - - setIsDragging(false) - - if (isSwipeRevealed) { - // When actions are revealed, check if user swiped right enough to hide - if (swipeTranslateX > -swipeThreshold) { - setSwipeTranslateX(0) - setIsSwipeRevealed(false) - } else { - // Snap back to revealed position - setSwipeTranslateX(-maxSwipeDistance) - } - } else { - // When actions are hidden, check if user swiped left enough to reveal - 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 (isMultiSelectMode || viewOnly || isSwipeRevealed || isTouchDevice) - return - const timer = setTimeout(() => { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - setHoverTimer(null) - }, 1500) - setHoverTimer(timer) - } - - const handleMouseLeave = () => { - if (isTouchDevice) return - - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - - // Add a small delay before hiding to allow moving to action area - if (isSwipeRevealed) { - const hideTimer = setTimeout(() => { - resetSwipe() - }, 300) - setHoverTimer(hideTimer) - } - } - - const handleActionAreaMouseEnter = () => { - if (isTouchDevice) return - - // Clear any pending timer when entering action area (both show and hide timers) - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - } - - const handleActionAreaMouseLeave = () => { - if (isTouchDevice) return - - // Hide immediately when leaving action area - if (isSwipeRevealed) { - resetSwipe() - } - } - - // Clean up timer on unmount - React.useEffect(() => { - return () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - } - } - }, [hoverTimer]) - // Check if the current user can approve/reject (admin, manager, or task owner) const canApproveReject = () => { if (!circleMembersData?.res || !chore) return false @@ -429,334 +224,120 @@ const CompactChoreCard = ({ } return ( - - - {/* Action buttons underneath (revealed on swipe) */} + + {/* Priority bar clickable area */} + {chore.priority > 0 && ( - {chore.status === 3 ? ( - // Pending approval: Show approve/reject for admins/managers/owners - canApproveReject() ? ( - <> - {/* - - */} - { - e.stopPropagation() - resetSwipe() - onAction('reject', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - ) : ( - - - - ) - ) : ( - { - e.stopPropagation() - resetSwipe() - - if (chore.status === 0 || chore.status === 2) { - onAction('start', chore) - } else { - onAction('complete', chore) - } - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - {chore.status !== 1 ? ( - - ) : ( - - )} - - )} - - { - e.stopPropagation() - resetSwipe() - onAction('changeDueDate', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - // bgcolor: 'warning.100', - // color: 'warning.600', - // '&:hover': { - // bgcolor: 'warning.200', - // }, - }} - > - - - - { - e.stopPropagation() - resetSwipe() - navigate(`/chores/${chore.id}/edit`) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - // bgcolor: 'neutral.100', - // color: 'neutral.600', - // '&:hover': { - // bgcolor: 'neutral.200', - // }, - }} - > - - - - {isOfficialInstance && ( - { - e.stopPropagation() - resetSwipe() - onAction('nudge', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - )} - - { - e.stopPropagation() - resetSwipe() - onAction('delete', chore) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - - {/* Main card content */} - { - if (isSwipeRevealed) { - resetSwipe() - return - } - if (isMultiSelectMode) { - onSelectionToggle() - } else { - navigate(`/chores/${chore.id}`) - } + onClick={e => { + e.stopPropagation() + onChipClick({ priority: chore.priority }) }} - onTouchStart={handleTouchStart} - onTouchMove={handleTouchMove} - onTouchEnd={handleTouchEnd} - onMouseDown={handleMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleMouseUp} - // onMouseEnter={handleMouseEnter} - > - {/* Priority bar clickable area */} - {chore.priority > 0 && ( - { - e.stopPropagation() - onChipClick({ priority: chore.priority }) - }} - /> - )} + /> + )} - {/* Animated transition container for Complete Button / Multi-select checkbox */} + {/* Animated transition container for Complete Button / Multi-select checkbox */} + + {/* Complete Button */} + {showActions && ( - {/* Complete Button */} - {showActions && ( - - {chore.status === 3 ? ( - // Pending approval: Show approve/reject for admins/managers/owners, grayed out for others - canApproveReject() ? ( - - { - e.stopPropagation() - onAction('approve', chore) - }} - sx={{ - width: 24, - height: 24, - borderRadius: '50%', - transition: 'all 0.2s ease', - '&:hover': { - transform: 'scale(1.05)', - }, - '&:active': { - transform: 'scale(0.95)', - }, - }} - > - - - {/* + { + e.stopPropagation() + onAction('approve', chore) + }} + sx={{ + width: 24, + height: 24, + borderRadius: '50%', + transition: 'all 0.2s ease', + '&:hover': { + transform: 'scale(1.05)', + }, + '&:active': { + transform: 'scale(0.95)', + }, + }} + > + + + {/* */} - - ) : ( - - - - ) - ) : ( - { - e.stopPropagation() - if (chore.status === 0) { - onAction('complete', chore) - } else if (chore.status === 1) { - onAction('pause', chore) - } else { - onAction('start', chore) - } - }} - disabled={notInCompletionWindow(chore)} - sx={{ - width: 32, - height: 32, - borderRadius: '50%', - transition: 'all 0.2s ease', - '&:hover': { - transform: 'scale(1.05)', - }, - - '&:active': { - transform: 'scale(0.95)', - }, - '&:disabled': { - opacity: 0.5, - transform: 'none', - }, - }} - > - {chore.status === 0 ? ( - - ) : chore.status === 1 ? ( - - ) : ( - - )} - - )} - - )} - {/* Multi-select Checkbox */} - - e.stopPropagation()} - /> - - - - {/* Content - Center */} - - {/* Line 1: Name + Due Date */} - - {/* Chore Name */} - - {chore.name} - - - {/* Due Date - Inline with name */} - - {getDueDateText(chore.nextDueDate)} - - - - {/* Line 2: Metadata */} - - {getFrequencyIcon(chore)} - - {formatMetadata()} - - - {/* Labels - Priority chip removed, now shown as vertical bar */} - {chore.labelsV2?.map(l => ( -
{ - e.stopPropagation() - onChipClick({ label: l }) + + ) : ( + { - if (e.key === 'Enter' || e.key === ' ') { - e.stopPropagation() - onChipClick({ label: l }) - } - }} - style={{ - cursor: 'pointer', - padding: 0, - margin: 0, - display: 'flex', - alignItems: 'center', - }} - key={`compact-chorecard-${chore.id}-label-${l.id}`} > - - {l?.name} - -
- ))} -
-
- - {/* Right side - Action Menu with animation */} - - {showActions && ( - onAction('completeWithNote', chore)} - onCompleteWithPastDate={() => - onAction('completeWithPastDate', chore) - } - onChangeAssignee={() => onAction('changeAssignee', chore)} - onChangeDueDate={() => onAction('changeDueDate', chore)} - onWriteNFC={() => onAction('writeNFC', chore)} - onNudge={() => onAction('nudge', chore)} - onDelete={() => onAction('delete', chore)} - onMouseEnter={handleMouseEnter} - // onMouseLeave={handleMouseLeave} + + + ) + ) : ( + { + e.stopPropagation() + if (chore.status === 0) { + onAction('complete', chore) + } else if (chore.status === 1) { + onAction('pause', chore) + } else { + onAction('start', chore) + } + }} + disabled={notInCompletionWindow(chore)} sx={{ width: 32, height: 32, - color: 'text.tertiary', - flexShrink: 0, + borderRadius: '50%', + transition: 'all 0.2s ease', '&:hover': { - color: 'text.secondary', - bgcolor: 'background.level1', + transform: 'scale(1.05)', + }, + + '&:active': { + transform: 'scale(0.95)', + }, + '&:disabled': { + opacity: 0.5, + transform: 'none', }, }} - onOpen={() => { - handleMouseLeave() - }} - /> + > + {chore.status === 0 ? ( + + ) : chore.status === 1 ? ( + + ) : ( + + )} + )} + )} + {/* Multi-select Checkbox */} + + e.stopPropagation()} + />
+ + {/* Content - Center */} + + {/* Line 1: Name + Due Date */} + + {/* Chore Name */} + + {chore.name} + + + {/* Due Date - Inline with name */} + + {getDueDateText(chore.nextDueDate)} + + + + {/* Line 2: Metadata */} + + {getFrequencyIcon(chore)} + + {formatMetadata()} + + + {/* Labels - Priority chip removed, now shown as vertical bar */} + {chore.labelsV2?.map(l => ( +
{ + e.stopPropagation() + onChipClick({ label: l }) + }} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation() + onChipClick({ label: l }) + } + }} + style={{ + cursor: 'pointer', + padding: 0, + margin: 0, + display: 'flex', + alignItems: 'center', + }} + key={`compact-chorecard-${chore.id}-label-${l.id}`} + > + + {l?.name} + +
+ ))} +
+
+ + {/* Right side - Action Menu with animation */} + + {showActions && ( + onAction('completeWithNote', chore)} + onCompleteWithPastDate={() => + onAction('completeWithPastDate', chore) + } + onChangeAssignee={() => onAction('changeAssignee', chore)} + onChangeDueDate={() => onAction('changeDueDate', chore)} + onWriteNFC={() => onAction('writeNFC', chore)} + onNudge={() => onAction('nudge', chore)} + onDelete={() => onAction('delete', chore)} + sx={{ + width: 32, + height: 32, + color: 'text.tertiary', + flexShrink: 0, + '&:hover': { + color: 'text.secondary', + bgcolor: 'background.level1', + }, + }} + /> + )} +
) } diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index cb8d3e7..71d9f4e 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -38,8 +38,6 @@ import Priorities from '../../utils/Priorities' import LoadingComponent from '../components/Loading' import { useLabels } from '../Labels/LabelQueries' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' -import ChoreCard from './ChoreCard' -import CompactChoreCard from './CompactChoreCard' import IconButtonWithMenu from './IconButtonWithMenu' import { useMediaQuery } from '@mui/material' @@ -60,6 +58,7 @@ import CalendarMonthly from '../components/CalendarMonthly.jsx' import ProjectSelector from '../components/ProjectSelector' import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder' import { useProjects } from '../Projects/ProjectQueries.js' +import ChoreListView from './ChoreListView.jsx' import ChoreModals from './components/ChoreModals' import FilterSection from './components/FilterSection' import MultiSelectToolbar from './components/MultiSelectToolbar' @@ -633,22 +632,60 @@ const MyChores = () => { } } - const renderChoreCard = (chore, key) => { - const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard - return ( - toggleChoreSelection(chore.id)} - /> - ) - } + // const renderChoreCard = (chore, key) => { + // const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard + // return ( + // toggleChoreSelection(chore.id)} + // /> + // ) + // } + // const renderChores = chores => { + // return ( + // + // {chores.map(chore => ( + // + // + // + // + // + // } + // > + // {/* */} + // {renderChoreCard(chore)} + // {/* {chores.map(chore => renderChoreCard(chore))} */} + // + // ))} + // + // ) + // } const getFilteredChores = useMemo(() => { if (activeFilterId || tempFilter) { @@ -1141,9 +1178,18 @@ const MyChores = () => {
)} {(searchTerm?.length > 0 || searchFilter !== 'All' || activeFilterId) && - viewMode !== 'calendar' && - getFilteredChores.map(chore => - renderChoreCard(chore, `filtered-${chore.id}`), + viewMode !== 'calendar' && ( + )} {viewMode === 'calendar' && ( <> @@ -1310,20 +1356,17 @@ const MyChores = () => { No tasks scheduled for this date ) : ( - getChoresForDate(selectedCalendarDate).map(chore => ( - toggleChoreSelection(chore.id)} - /> - )) + )}
@@ -1396,7 +1439,17 @@ const MyChores = () => { }, }} > - {section.content?.map(chore => renderChoreCard(chore))} + ) diff --git a/src/views/Filters/FilterView.jsx b/src/views/Filters/FilterView.jsx index 87b9057..7c7505d 100644 --- a/src/views/Filters/FilterView.jsx +++ b/src/views/Filters/FilterView.jsx @@ -10,17 +10,18 @@ import { Stack, Typography, } from '@mui/joy' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' - import { - Add, - FilterAlt, - MoreVert, - Star, - StarBorder, - Task, -} from '@mui/icons-material' + Type as ListType, + SwipeableList, + SwipeableListItem, + SwipeAction, + TrailingActions, +} from 'react-swipeable-list' +import 'react-swipeable-list/dist/styles.css' + +import { Add, FilterAlt, Star, StarBorder, Task } from '@mui/icons-material' import { useChores } from '../../queries/ChoreQueries' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries' import { getFilterCount, getFilterOverdueCount } from '../../utils/FilterEngine' @@ -31,177 +32,14 @@ import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import { useProjects } from '../Projects/ProjectQueries' import { - useFilters, useCreateFilter, - useUpdateFilter, useDeleteFilter, + useFilters, useToggleFilterPin, + useUpdateFilter, } from './FilterQueries' -const FilterCard = ({ - filter, - onEditClick, - onDeleteClick, - onPinClick, - taskCount = 0, - overdueCount = 0, -}) => { - const navigate = useNavigate() - - // 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 = 200 // Increased to fit pin + edit + delete - 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) - }, 800) - setHoverTimer(timer) - } - - const handleMouseLeave = () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - if (!isSwipeRevealed) { - const hideTimer = setTimeout(() => { - resetSwipe() - }, 300) - setHoverTimer(hideTimer) - } - } - - const handleActionAreaMouseEnter = () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - } - - const handleActionAreaMouseLeave = () => { - if (isSwipeRevealed) { - resetSwipe() - } - } - - // Clean up timer on unmount - useEffect(() => { - return () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - } - } - }, [hoverTimer]) - +const FilterCardContent = ({ filter, taskCount = 0, overdueCount = 0 }) => { // Get condition labels for display const getConditionSummary = () => { if (!filter.conditions || filter.conditions.length === 0) { @@ -214,330 +52,164 @@ const FilterCard = ({ } return ( - + + {/* Filter Icon */} { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } + display: 'flex', + alignItems: 'center', + mr: 2, + flexShrink: 0, }} > - {/* Action buttons underneath (revealed on swipe) */} - - { - e.stopPropagation() - resetSwipe() - onPinClick(filter.id) - }} - sx={{ - width: 40, - height: 40, - mx: 0.5, - }} - > - {filter.isPinned ? ( - - ) : ( - - )} - + {''} + + - { - e.stopPropagation() - resetSwipe() - onEditClick(filter) - }} + {/* Content - Center */} + + {/* Filter Name */} + + - - - - { - e.stopPropagation() - resetSwipe() - onDeleteClick(filter.id) - }} - sx={{ - width: 40, - height: 40, - mx: 0.5, - }} - > - - + {filter.name} + + {filter.isPinned && ( + + )} - {/* Main card content */} + {/* Filter Info */} { - if (isSwipeRevealed) { - resetSwipe() - return - } - // Navigate to MyChores with filter applied via URL param - navigate(`/chores?filterId=${encodeURIComponent(filter.id)}`) - }} - onTouchStart={handleTouchStart} - onTouchMove={handleTouchMove} - onTouchEnd={handleTouchEnd} - onMouseDown={handleMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleMouseUp} > - {/* Right drag area */} - { - e.stopPropagation() - const clampedDelta = isSwipeRevealed ? 0 : -maxSwipeDistance - setSwipeTranslateX(clampedDelta) - }} - > - {/* Drag indicator dots */} - - - - + {filter.description} + + )} - {/* Filter Icon */} - } + color={overdueCount > 0 ? 'danger' : 'primary'} sx={{ - display: 'flex', - alignItems: 'center', - mr: 2, - flexShrink: 0, + fontSize: 10, + height: 18, + px: 0.75, + bgcolor: overdueCount > 0 ? 'danger.softBg' : 'primary.softBg', + color: overdueCount > 0 ? 'danger.500' : 'primary.500', }} > - + + {overdueCount > 0 && ( + - {''} - - + {overdueCount} overdue +
+ )} - {/* Content - Center */} - } sx={{ - flex: 1, - minWidth: 0, - display: 'flex', - flexDirection: 'column', + fontSize: 10, + height: 18, + px: 0.75, + bgcolor: 'neutral.softBg', + color: 'neutral.600', }} > - {/* Filter Name */} - - - {filter.name} - - {filter.isPinned && ( - - )} - + {getConditionSummary()} + - {/* Filter Info */} - 0 && ( + - {filter.description && ( - - {filter.description} - - )} - - } - color={overdueCount > 0 ? 'danger' : 'primary'} - sx={{ - fontSize: 10, - height: 18, - px: 0.75, - bgcolor: - overdueCount > 0 ? 'danger.softBg' : 'primary.softBg', - color: overdueCount > 0 ? 'danger.500' : 'primary.500', - }} - > - {taskCount} tasks - - - {overdueCount > 0 && ( - - {overdueCount} overdue - - )} - - } - sx={{ - fontSize: 10, - height: 18, - px: 0.75, - bgcolor: 'neutral.softBg', - color: 'neutral.600', - }} - > - {getConditionSummary()} - - - {filter.usageCount > 0 && ( - - Used {filter.usageCount}x - - )} - - + Used {filter.usageCount}x + + )}
@@ -545,6 +217,7 @@ const FilterCard = ({ } const FilterView = () => { + const navigate = useNavigate() const { data: userProfile } = useUserProfile() const { data: chores = { res: [] } } = useChores(false) const { data: labels = [] } = useLabels() @@ -613,14 +286,7 @@ const FilterView = () => { setFilterCounts(counts) } - }, [ - chores, - filtersData, - userProfile?.id, - labels, - projects, - membersData?.res, - ]) + }, [chores, filtersData, userProfile?.id, labels, projects, membersData?.res]) const handleAddFilter = () => { setEditingFilter(null) @@ -748,17 +414,100 @@ const FilterView = () => { ) : ( - savedFilters.map(filter => ( - - )) + + {savedFilters.map(filter => ( + + navigate(`/chores?filterId=${encodeURIComponent(filter.id)}`) + } + key={filter.id} + trailingActions={ + + + handlePinFilter(filter.id)}> + + {filter.isPinned ? ( + + ) : ( + + )} + + {filter.isPinned ? 'Unpin' : 'Pin'} + + + + handleEditFilter(filter)}> + + + + Edit + + + + handleDeleteClicked(filter.id)} + > + + + + Delete + + + + + + } + > + + + ))} + )} diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index 037824a..de8d02b 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -8,27 +8,28 @@ import { Timelapse, TrendingUp, } from '@mui/icons-material' -import { - Box, - Button, - Card, - Container, - Grid, - List, - Sheet, - Typography, -} from '@mui/joy' +import DeleteIcon from '@mui/icons-material/Delete' +import EditIcon from '@mui/icons-material/Edit' +import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy' import moment from 'moment' import { useEffect, useState } from 'react' import { Link, useParams } from 'react-router-dom' +import { + Type as ListType, + SwipeableList, + SwipeableListItem, + SwipeAction, + TrailingActions, +} from 'react-swipeable-list' +import 'react-swipeable-list/dist/styles.css' import useConfirmationModal from '../../hooks/useConfirmationModal' -import { ChoreHistoryStatus } from '../../utils/Chores' import { useChoreHistory, useDeleteChoreHistory, useUpdateChoreHistory, } from '../../queries/ChoreQueries' import { useCircleMembers } from '../../queries/UserQueries' +import { ChoreHistoryStatus } from '../../utils/Chores' import LoadingComponent from '../components/Loading' import EditHistoryModal from '../Modals/EditHistoryModal' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' @@ -287,23 +288,76 @@ const ChoreHistory = () => { Task Activity - + {/* Chore History List (Updated Style) */} - + {choreHistory.map((historyEntry, index) => ( - handleEdit(historyEntry)} - onEditClick={handleEdit} - onDeleteClick={handleDelete} - historyEntry={historyEntry} - performers={performers} - allHistory={choreHistory} - key={index} - index={index} - /> + + + handleEdit(historyEntry)}> + + + + Edit + + + + handleDelete(historyEntry)}> + + + + Delete + + + + + + } + > + + ))} - + { @@ -96,30 +83,12 @@ const formatTime = seconds => { } /** - * Compact HistoryCard component with improved UX and 2-row height design + * Compact HistoryCard component - content only */ -const HistoryCard = ({ - allHistory, - performers, - historyEntry, - index, - onClick, - onEditClick, - onDeleteClick, -}) => { +const HistoryCard = ({ allHistory, performers, historyEntry, index }) => { const performer = performers.find(p => p.userId === historyEntry.completedBy) const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo) - // 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 = 200 - const dragStartX = useRef(0) - const cardRef = useRef(null) - const formatTimeDifference = (startDate, endDate) => { const diffInMinutes = moment(startDate).diff(endDate, 'minutes') let timeValue = diffInMinutes @@ -166,477 +135,154 @@ const HistoryCard = ({ ) } - // 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 - only trigger from drag area - const handleMouseEnter = () => { - if (isSwipeRevealed) return - const timer = setTimeout(() => { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - setHoverTimer(null) - }, 800) // Shorter delay for drag area - setHoverTimer(timer) - } - - const handleMouseLeave = () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - // Only add hide timer if we're leaving the drag area and actions are NOT revealed - // If actions are revealed, let the action area handle the hiding - if (!isSwipeRevealed) { - // Actions are not revealed, so we can safely hide after delay - const hideTimer = setTimeout(() => { - resetSwipe() - }, 300) - setHoverTimer(hideTimer) - } - } - - const handleActionAreaMouseEnter = () => { - // Clear any pending timer when entering action area - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - } - - const handleActionAreaMouseLeave = () => { - // Hide immediately when leaving action area - if (isSwipeRevealed) { - resetSwipe() - } - } - - // Clean up timer on unmount - useEffect(() => { - return () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - } - } - }, [hoverTimer]) - return ( - <> - { - // Only clear timers, don't auto-hide - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - }} - > - {/* Action buttons underneath (revealed on swipe) */} - {(onEditClick || onDeleteClick) && ( - - {onEditClick && ( - { - e.stopPropagation() - resetSwipe() - onEditClick(historyEntry) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - )} - - {onDeleteClick && ( - { - e.stopPropagation() - resetSwipe() - onDeleteClick(historyEntry) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - )} - - )} - - {/* Main card content */} - { - if (isSwipeRevealed) { - resetSwipe() - return - } - if (onClick) onClick() - }} - sx={{ - cursor: onClick ? 'pointer' : 'default', - py: 1.5, - px: 2, - position: 'relative', - bgcolor: 'background.surface', - transform: `translateX(${swipeTranslateX}px)`, - transition: isDragging ? 'none' : 'transform 0.3s ease-out', - zIndex: 1, - width: '100%', - '&:hover': onClick - ? { - bgcolor: isSwipeRevealed - ? 'background.surface' - : 'background.level1', - } - : {}, - borderRadius: 'sm', - }} - onTouchStart={handleTouchStart} - onTouchMove={handleTouchMove} - onTouchEnd={handleTouchEnd} - onMouseDown={handleMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleMouseUp} - > - - - {/* First Row/Column: Status and Time Info */} - - - {getStatusAvatar()} - - - {historyEntry.status === 0 - ? 'In Progress' - : historyEntry.status === 1 - ? 'Completed' - : historyEntry.status === 2 - ? 'Skipped' - : historyEntry.status === 3 - ? 'Pending Approval' - : historyEntry.status === 4 - ? 'Rejected' - : 'Completed'} - - - }> - {moment( - historyEntry.performedAt || historyEntry.updatedAt, - ).format('MMM DD, h:mm A')} - - - - {getCompletedChip(historyEntry)} - - - - - {/* Second Row/Column: Completion Status (right side on desktop) */} - - - {historyEntry.dueDate && ( - }> - {moment(historyEntry.dueDate).format('MMM DD h:mm A')} - - )} - - - - {/* Third Row: Performer and Assignment Info */} - - - } - > - Done by {performer?.displayName || 'Unknown'} - - - {historyEntry.completedBy !== historyEntry.assignedTo && - assignedTo && ( - } - > - Assigned to {assignedTo.displayName} - - )} - - {historyEntry.notes && ( - } - sx={{ maxWidth: '120px', overflow: 'hidden' }} - > - Note - - )} - {/* add a duration chip if we have duration */} - {historyEntry?.duration > 0 && ( - } - > - {formatTime(historyEntry.duration)} - - )} - {historyEntry?.points > 0 && ( - } - > - {historyEntry.points} pt - {historyEntry.points > 1 ? 's' : ''} - - )} - - - - - - {/* Right drag area - only triggers reveal on hover */} - {(onEditClick || onDeleteClick) && ( + + + + {/* First Row/Column: Status and Time Info */} + - {/* Drag indicator dots */} - - {[...Array(3)].map((_, i) => ( - - ))} + {historyEntry.status === 0 + ? 'In Progress' + : historyEntry.status === 1 + ? 'Completed' + : historyEntry.status === 2 + ? 'Skipped' + : historyEntry.status === 3 + ? 'Pending Approval' + : historyEntry.status === 4 + ? 'Rejected' + : 'Completed'} + + + }> + {moment( + historyEntry.performedAt || historyEntry.updatedAt, + ).format('MMM DD, h:mm A')} + + + + {getCompletedChip(historyEntry)} - )} - + - {/* Compact Divider with Time Difference */} - {index < allHistory.length - 1 && allHistory[index + 1].performedAt && ( - - + - {formatTimeDifference( - historyEntry.performedAt || historyEntry.updatedAt, - allHistory[index + 1].performedAt, - )}{' '} - before - - - )} + {historyEntry.dueDate && ( + }> + {moment(historyEntry.dueDate).format('MMM DD h:mm A')} + + )} + + + + {/* Third Row: Performer and Assignment Info */} + + + } + > + Done by {performer?.displayName || 'Unknown'} + + + {historyEntry.completedBy !== historyEntry.assignedTo && + assignedTo && ( + } + > + Assigned to {assignedTo.displayName} + + )} + + {historyEntry.notes && ( + } + sx={{ maxWidth: '120px', overflow: 'hidden' }} + > + Note + + )} + {/* add a duration chip if we have duration */} + {historyEntry?.duration > 0 && ( + } + > + {formatTime(historyEntry.duration)} + + )} + {historyEntry?.points > 0 && ( + } + > + {historyEntry.points} pt + {historyEntry.points > 1 ? 's' : ''} + + )} + + + - + ) } diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx index bd82a61..2986dc5 100644 --- a/src/views/Labels/LabelView.jsx +++ b/src/views/Labels/LabelView.jsx @@ -10,429 +10,122 @@ import { Stack, Typography, } from '@mui/joy' -import { useEffect, useRef, useState } from 'react' +import { useEffect, 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 { + Type as ListType, + SwipeableList, + SwipeableListItem, + SwipeAction, + TrailingActions, +} from 'react-swipeable-list' +import 'react-swipeable-list/dist/styles.css' import { useUserProfile } from '../../queries/UserQueries' -import LABEL_COLORS, { - getTextColorFromBackgroundColor, -} from '../../utils/Colors' +import { getTextColorFromBackgroundColor } from '../../utils/Colors' import { DeleteLabel } from '../../utils/Fetcher' import { getSafeBottomStyles } from '../../utils/SafeAreaUtils' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import { useLabels } from './LabelQueries' -const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => { - // 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 - } - +const LabelCardContent = ({ label, currentUserId }) => { // Check if current user owns this label const isOwnedByCurrentUser = label.created_by === currentUserId - // 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 - only trigger from drag area - const handleMouseEnter = () => { - if (isSwipeRevealed) return - const timer = setTimeout(() => { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - setHoverTimer(null) - }, 800) // Shorter delay for drag area - setHoverTimer(timer) - } - - const handleMouseLeave = () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - // Only add hide timer if we're leaving the drag area and actions are NOT revealed - // If actions are revealed, let the action area handle the hiding - if (!isSwipeRevealed) { - // Actions are not revealed, so we can safely hide after delay - const hideTimer = setTimeout(() => { - resetSwipe() - }, 300) - setHoverTimer(hideTimer) - } - } - - const handleActionAreaMouseEnter = () => { - // Clear any pending timer when entering action area - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - } - - const handleActionAreaMouseLeave = () => { - // Hide immediately when leaving action area - if (isSwipeRevealed) { - resetSwipe() - } - } - - // Clean up timer on unmount - useEffect(() => { - return () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - } - } - }, [hoverTimer]) - return ( - + + {/* Color Avatar */} { - // Only clear timers, don't auto-hide - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } + display: 'flex', + alignItems: 'center', + mr: 2, + flexShrink: 0, }} > - {/* Action buttons underneath (revealed on swipe) */} - - { - e.stopPropagation() - resetSwipe() - onEditClick(label) - }} + - - + {label.name.charAt(0).toUpperCase()} + + + - { - e.stopPropagation() - resetSwipe() - onDeleteClick(label.id) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - - {/* Main card content */} - + {/* Label Name */} + { - 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} > - {/* Right drag area - only triggers reveal on hover */} - - {/* Drag indicator dots */} - - {[...Array(3)].map((_, i) => ( - - ))} - - - {/* Color Avatar */} - - + + {/* Color Info */} + + {!isOwnedByCurrentUser && ( + - - {label.name.charAt(0).toUpperCase()} - - - - - {/* Content - Center */} - - {/* Label Name */} - - {label.name} - - - {/* Color Info */} - - {!isOwnedByCurrentUser && ( - - Shared - - )} - - + Shared + + )} @@ -545,10 +238,6 @@ const LabelView = () => { @@ -567,15 +256,64 @@ const LabelView = () => { )} - {userLabels.map(label => ( - - ))} + + {userLabels.map(label => ( + + + handleEditLabel(label)}> + + + + Edit + + + + handleDeleteClicked(label.id)}> + + + + Delete + + + + + + } + > + + + ))} + {modalOpen && ( diff --git a/src/views/Projects/ProjectView.jsx b/src/views/Projects/ProjectView.jsx index 65c103f..3c27ba9 100644 --- a/src/views/Projects/ProjectView.jsx +++ b/src/views/Projects/ProjectView.jsx @@ -10,506 +10,191 @@ import { Stack, Typography, } from '@mui/joy' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import ProjectModal from '../Modals/Inputs/ProjectModal' import { Add, Task } from '@mui/icons-material' import { useQueryClient } from '@tanstack/react-query' +import { + Type as ListType, + SwipeableList, + SwipeableListItem, + SwipeAction, + TrailingActions, +} from 'react-swipeable-list' +import 'react-swipeable-list/dist/styles.css' import { useChores } from '../../queries/ChoreQueries' import { useUserProfile } from '../../queries/UserQueries' -import LABEL_COLORS, { - getTextColorFromBackgroundColor, -} from '../../utils/Colors' +import { getTextColorFromBackgroundColor } from '../../utils/Colors' import { DeleteProject } from '../../utils/Fetcher' import { getIconComponent } from '../../utils/ProjectIcons' import { getSafeBottomStyles } from '../../utils/SafeAreaUtils' import { useProjectFilter } from '../Chores/hooks/useProjectFilter' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import { useProjects } from './ProjectQueries' -const ProjectCard = ({ +const ProjectCardContent = ({ project, - onEditClick, - onDeleteClick, - isEditable = true, currentUserId, taskCounts = {}, + onCardClick, }) => { - const navigate = useNavigate() - const { data: projects = [], isLoading: projectsLoading } = useProjects() - // Helper function to get color name from hex value - const { setSelectedProjectWithCache } = useProjectFilter(projects) - const getColorName = hexValue => { - const colorObj = LABEL_COLORS.find( - color => color.value.toLowerCase() === hexValue.toLowerCase(), - ) - return colorObj ? colorObj.name : hexValue - } - // Check if current user owns this project const isOwnedByCurrentUser = project.created_by === currentUserId const isDefaultProject = project.id === 'default' const taskCount = taskCounts[project.id] || 0 - // 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 (same as LabelView) - 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) - }, 800) - setHoverTimer(timer) - } - - const handleMouseLeave = () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - if (!isSwipeRevealed) { - const hideTimer = setTimeout(() => { - resetSwipe() - }, 300) - setHoverTimer(hideTimer) - } - } - - const handleActionAreaMouseEnter = () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - } - - const handleActionAreaMouseLeave = () => { - if (isSwipeRevealed) { - resetSwipe() - } - } - - // Clean up timer on unmount - useEffect(() => { - return () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - } - } - }, [hoverTimer]) - return ( - + + {/* Project Avatar */} { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } + display: 'flex', + alignItems: 'center', + mr: 2, + flexShrink: 0, }} > - {/* Action buttons underneath (revealed on swipe) */} - {isEditable && ( - - { - e.stopPropagation() - resetSwipe() - onEditClick(project) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - {/* Only show delete for non-default projects */} - {!isDefaultProject && ( - { - e.stopPropagation() - resetSwipe() - onDeleteClick(project.id) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - )} - - )} - - {/* Main card content */} - { - if (isSwipeRevealed) { - resetSwipe() - return - } - // Always navigate to MyChores with project filter when clicking on the card - // For default project, use 'default', for others use project ID - const projectIdentifier = - project.id === 'default' ? 'default' : project.id - setSelectedProjectWithCache(project) - navigate(`/chores?project=${encodeURIComponent(projectIdentifier)}`) - }} - onTouchStart={isEditable ? handleTouchStart : undefined} - onTouchMove={isEditable ? handleTouchMove : undefined} - onTouchEnd={isEditable ? handleTouchEnd : undefined} - onMouseDown={isEditable ? handleMouseDown : undefined} - onMouseMove={isEditable ? handleMouseMove : undefined} - onMouseUp={isEditable ? handleMouseUp : undefined} > - {/* Right drag area */} - {isEditable && ( - - {/* Drag indicator dots */} - - {[...Array(3)].map((_, i) => ( - - ))} - - + {project.icon ? ( + (() => { + const IconComponent = getIconComponent(project.icon) + return ( + + ) + })() + ) : ( + <> )} + + - {/* Project Avatar */} - - + {/* Project Name */} + + {project.name} + {isDefaultProject && ( + - {project.icon ? ( - (() => { - const IconComponent = getIconComponent(project.icon) - return ( - - ) - })() - ) : ( - <> - )} - - + Default + + )} + - {/* Content - Center */} - - {/* Project Name */} + {/* Project Info */} + + {project.description && ( - {project.name} - {isDefaultProject && ( - - Default - - )} + {project.description} + )} - {/* Project Info */} - - {project.description && ( - - {project.description} - - )} + } + sx={{ + fontSize: 10, + height: 18, + px: 0.75, + bgcolor: 'primary.softBg', + color: 'primary.500', + }} + > + {taskCount} tasks + - } - sx={{ - fontSize: 10, - height: 18, - px: 0.75, - bgcolor: 'primary.softBg', - color: 'primary.500', - }} - > - {taskCount} tasks - - - {!isOwnedByCurrentUser && !isDefaultProject && ( - - Shared - - )} - - + {!isOwnedByCurrentUser && !isDefaultProject && ( + + Shared + + )} @@ -520,6 +205,9 @@ const ProjectView = () => { const { data: projects, isProjectsLoading, isError } = useProjects() const { data: userProfile } = useUserProfile() const { data: chores = { res: [] } } = useChores(false) // false to exclude archived + const { data: projectsData = [], isLoading: projectsLoading } = useProjects() + const { setSelectedProjectWithCache } = useProjectFilter(projectsData) + const navigate = useNavigate() const [userProjects, setUserProjects] = useState([]) const [modalOpen, setModalOpen] = useState(false) @@ -568,6 +256,14 @@ const ProjectView = () => { setModalOpen(false) } + const handleCardClick = project => { + // Always navigate to MyChores with project filter when clicking on the card + // For default project, use 'default', for others use project ID + const projectIdentifier = project.id === 'default' ? 'default' : project.id + setSelectedProjectWithCache(project) + navigate(`/chores?project=${encodeURIComponent(projectIdentifier)}`) + } + useEffect(() => { if (projects) { setUserProjects(projects) @@ -649,32 +345,95 @@ const ProjectView = () => { overflow: 'hidden', }} > - {/* default project: */} - {}} taskCounts={{ default: taskCounts.default || 0 }} + onCardClick={() => + handleCardClick({ + id: 'default', + name: 'Default Project', + icon: 'FolderOpen', + color: '#1976d2', + }) + } /> - {userProjects.map(project => ( - - ))} + + {/* User projects - swipeable */} + + {userProjects.map(project => ( + handleCardClick(project)} + key={project.id} + trailingActions={ + + + handleEditProject(project)}> + + + + Edit + + + + handleDeleteClicked(project.id)} + > + + + + Delete + + + + + + } + > + + + ))} + {modalOpen && ( diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx index 0e4791f..9f7b08a 100644 --- a/src/views/Things/ThingsView.jsx +++ b/src/views/Things/ThingsView.jsx @@ -17,8 +17,16 @@ import { Stack, Typography, } from '@mui/joy' -import React, { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { + Type as ListType, + SwipeableList, + SwipeableListItem, + SwipeAction, + TrailingActions, +} from 'react-swipeable-list' +import 'react-swipeable-list/dist/styles.css' import { useNotification } from '../../service/NotificationProvider' import { CreateThing, @@ -31,25 +39,8 @@ import { getSafeBottomStyles } from '../../utils/SafeAreaUtils' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import CreateThingModal from '../Modals/Inputs/CreateThingModal' import EditThingStateModal from '../Modals/Inputs/EditThingState' -const ThingCard = ({ - thing, - onEditClick, - onStateChangeRequest, - onDeleteClick, -}) => { - const [isDisabled, setIsDisabled] = useState(false) - const Navigate = useNavigate() - - // 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 = 200 - const dragStartX = useRef(0) - const cardRef = useRef(null) +const ThingCardContent = ({ thing, onCardClick }) => { const getThingIcon = type => { if (type === 'text') { return @@ -93,425 +84,103 @@ const ThingCard = ({ ) } - const handleRequestChange = thing => { - setIsDisabled(true) - resetSwipe() - onStateChangeRequest(thing) - setTimeout(() => { - setIsDisabled(false) - }, 2000) - } - - // 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 - only trigger from drag area - const handleMouseEnter = () => { - if (isSwipeRevealed) return - const timer = setTimeout(() => { - setSwipeTranslateX(-maxSwipeDistance) - setIsSwipeRevealed(true) - setHoverTimer(null) - }, 800) // Shorter delay for drag area - setHoverTimer(timer) - } - - const handleMouseLeave = () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - // Only add hide timer if we're leaving the drag area and actions are NOT revealed - // If actions are revealed, let the action area handle the hiding - if (!isSwipeRevealed) { - // Actions are not revealed, so we can safely hide after delay - const hideTimer = setTimeout(() => { - resetSwipe() - }, 300) - setHoverTimer(hideTimer) - } - } - - const handleActionAreaMouseEnter = () => { - // Clear any pending timer when entering action area - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } - } - - const handleActionAreaMouseLeave = () => { - // Hide immediately when leaving action area - if (isSwipeRevealed) { - resetSwipe() - } - } - - // Clean up timer on unmount - React.useEffect(() => { - return () => { - if (hoverTimer) { - clearTimeout(hoverTimer) - } - } - }, [hoverTimer]) - return ( - + + {/* Avatar and Primary Action */} { - // Only clear timers, don't auto-hide - if (hoverTimer) { - clearTimeout(hoverTimer) - setHoverTimer(null) - } + display: 'flex', + alignItems: 'center', + mr: 2, + flexShrink: 0, }} > - {/* Action buttons underneath (revealed on swipe) */} + {getThingAvatar()} + + + {/* Content - Center */} + + {/* Line 1: Name + State */} - { - e.stopPropagation() - if (thing?.type === 'text') { - onEditClick(thing) - } else { - handleRequestChange(thing) - } - }} - disabled={isDisabled} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - {getThingIcon(thing?.type)} - - - { - e.stopPropagation() - resetSwipe() - onEditClick(thing) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - { - e.stopPropagation() - resetSwipe() - onDeleteClick(thing) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - - {/* Main card content */} - { - if (isSwipeRevealed) { - resetSwipe() - return - } - Navigate(`/things/${thing?.id}`) - }} - onTouchStart={handleTouchStart} - onTouchMove={handleTouchMove} - onTouchEnd={handleTouchEnd} - onMouseDown={handleMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleMouseUp} > - {/* Right drag area - only triggers reveal on hover */} - - {/* Drag indicator dots */} - - {[...Array(3)].map((_, i) => ( - - ))} - - - {/* Avatar and Primary Action */} - - {getThingAvatar()} - - - {/* Content - Center */} - - {/* Line 1: Name + State */} - - - {thing?.name} - + {thing?.name} + - - {thing?.state} - - + + {thing?.state} + + - {/* Line 2: Type */} - - - {thing?.type} - - - + {/* Line 2: Type */} + + + {thing?.type} + @@ -519,6 +188,7 @@ const ThingCard = ({ } const ThingsView = () => { + const navigate = useNavigate() const [things, setThings] = useState([]) const [isShowCreateThingModal, setIsShowCreateThingModal] = useState(false) const [isShowEditThingStateModal, setIsShowEditStateModal] = useState(false) @@ -626,22 +296,23 @@ const ThingsView = () => { } const handleStateChangeRequest = thing => { - if (thing?.type === 'number') { - thing.state = Number(thing.state) + 1 - } else if (thing?.type === 'boolean') { - if (thing.state === 'true') { - thing.state = 'false' + const updatedThing = { ...thing } + if (updatedThing?.type === 'number') { + updatedThing.state = Number(updatedThing.state) + 1 + } else if (updatedThing?.type === 'boolean') { + if (updatedThing.state === 'true') { + updatedThing.state = 'false' } else { - thing.state = 'true' + updatedThing.state = 'true' } } - UpdateThingState(thing) + UpdateThingState(updatedThing) .then(result => { result.json().then(data => { const currentThings = [...things] const thingIndex = currentThings.findIndex( - currentThing => currentThing.id === thing.id, + currentThing => currentThing.id === updatedThing.id, ) currentThings[thingIndex] = data.res setThings(currentThings) @@ -712,15 +383,100 @@ const ThingsView = () => { )} - {things.map(thing => ( - - ))} + + {things.map(thing => ( + navigate(`/things/${thing?.id}`)} + key={thing.id} + trailingActions={ + + + { + if (thing?.type === 'text') { + handleEditClick(thing) + } else { + handleStateChangeRequest(thing) + } + }} + > + + {thing?.type === 'text' ? ( + + ) : thing?.type === 'number' ? ( + + ) : thing.state === 'true' ? ( + + ) : ( + + )} + + {thing?.type === 'text' ? 'Edit' : 'Toggle'} + + + + handleEditClick(thing)}> + + + + Edit + + + + handleDeleteClick(thing)}> + + + + Delete + + + + + + } + > + + + ))} + { 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) - // Fetch circle members data const { data: circleMembersData, isLoading: isCircleMembersLoading } = useCircleMembers() @@ -73,14 +75,6 @@ const TimerDetails = () => { return members?.find(member => member.userId === userId) } - // Detect if device supports touch - useEffect(() => { - const checkTouchDevice = () => { - setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0) - } - checkTouchDevice() - }, []) - // Update timerData when choreTimer data changes useEffect(() => { if (choreTimer?.res) { @@ -102,7 +96,6 @@ const TimerDetails = () => { } }, [timerData]) - const formatTime = seconds => { const hours = Math.floor(seconds / 3600) const minutes = Math.floor((seconds % 3600) / 60) @@ -341,151 +334,11 @@ 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', @@ -493,11 +346,6 @@ const TimerDetails = () => { }) } - // Reset swipes when editing mode changes - useEffect(() => { - resetAllSwipes() - }, [editingSessions]) - if (loading || isCircleMembersLoading) { return } @@ -1067,13 +915,7 @@ const TimerDetails = () => { Work Sessions ({timerData.pauseLog.length}) - + {timerData.pauseLog .sort((a, b) => moment(b.start) - moment(a.start)) .map((pause, pauseIndex) => { @@ -1095,67 +937,74 @@ const TimerDetails = () => { ) : pause.duration - const swipeState = getSessionSwipeState(pauseIndex) - return ( - + + handleEditSession()} + > + + + + Edit + + + + + handleDeleteSession(pauseIndex) + } + > + + + + Delete + + + + + + } > - {/* Action buttons underneath (revealed on swipe) */} - - { - e.stopPropagation() - handleEditSession() - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - { - e.stopPropagation() - handleDeleteSession(pauseIndex) - }} - sx={{ - width: 40, - height: 40, - mx: 1, - }} - > - - - - - {/* Session Card */} + {/* Session Card Content */} { borderColor: isOngoing ? 'success.300' : 'divider', - position: 'relative', - transform: `translateX(${swipeState.translateX}px)`, - transition: isDragging - ? 'none' - : 'transform 0.3s ease-out', - zIndex: 1, - cursor: 'pointer', - '&:hover': { - bgcolor: swipeState.isRevealed - ? 'background.surface' - : 'background.level1', - }, + borderRadius: 0, + borderBottom: '1px solid', + minWidth: '100%', }} - onClick={() => { - 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 */} { {endTime ? `→ ${endTime}` : '→ ongoing'} - - {/* Right drag indicator (desktop only) */} - {!isTouchDevice && ( - - {/* Drag indicator dots */} - - {[...Array(3)].map((_, i) => ( - - ))} - - - )} - + ) })} - + )}