From 0102ce2144f1cd0d9e47550fa77f7f7ac7f45d18 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Thu, 21 Aug 2025 00:34:00 -0400 Subject: [PATCH] Add BottomSheetModal component and implement calendar view in MyChores --- src/components/common/BottomSheetModal.jsx | 224 +++++++++++++++++ src/views/Chores/MyChores.jsx | 277 +++++++++++++++------ src/views/components/CalendarDual.jsx | 1 - src/views/components/CalendarView.jsx | 1 - 4 files changed, 426 insertions(+), 77 deletions(-) create mode 100644 src/components/common/BottomSheetModal.jsx diff --git a/src/components/common/BottomSheetModal.jsx b/src/components/common/BottomSheetModal.jsx new file mode 100644 index 0000000..2bd3884 --- /dev/null +++ b/src/components/common/BottomSheetModal.jsx @@ -0,0 +1,224 @@ +import { Close } from '@mui/icons-material' +import { IconButton, Modal, Sheet, Typography } from '@mui/joy' +import { forwardRef, useEffect, useState } from 'react' +import { Z_INDEX } from '../../constants/zIndex' + +const BottomSheetModal = forwardRef( + ( + { + open, + onClose, + children, + title, + height = 'auto', + maxHeight = '90vh', + expandedHeight = '95vh', + backdropBlur = true, + showHandle = true, + showCloseButton = true, + ...props + }, + ref, + ) => { + const [isExpanded, setIsExpanded] = useState(false) + const [isClosing, setIsClosing] = useState(false) + const [internalOpen, setInternalOpen] = useState(open) + + // Handle opening + useEffect(() => { + if (open) { + setInternalOpen(true) + setIsClosing(false) + } + }, [open]) + + // Handle closing with animation + useEffect(() => { + if (!open && internalOpen) { + setIsClosing(true) + // Wait for animation to complete before hiding modal + const timer = setTimeout(() => { + setInternalOpen(false) + setIsClosing(false) + setIsExpanded(false) + }, 250) // Match transition duration + + return () => clearTimeout(timer) + } + }, [open, internalOpen]) + + // Handle toggle expansion + const handleToggleExpansion = () => { + setIsExpanded(prev => !prev) + } + + // Close on escape key + useEffect(() => { + const handleEscape = event => { + if (event.key === 'Escape' && internalOpen) { + onClose?.() + } + } + + if (internalOpen) { + document.addEventListener('keydown', handleEscape) + // Prevent body scroll when modal is open + document.body.style.overflow = 'hidden' + } + + return () => { + document.removeEventListener('keydown', handleEscape) + document.body.style.overflow = 'unset' + } + }, [internalOpen, onClose]) + + // Calculate current height + const currentHeight = isExpanded ? expandedHeight : height + + return ( + + + {/* Header Section with drag handle, title, and close button */} +
+ {/* Close button positioned absolutely in top-right */} + {showCloseButton && ( + + + + )} + + {/* Drag Handle */} + {showHandle && ( +
+
+
+ )} + + {/* Title Row */} + {title && ( +
+ + {title} + +
+ )} +
+ {/* Content area */} +
+ {children} +
+ + + ) + }, +) + +BottomSheetModal.displayName = 'BottomSheetModal' + +export default BottomSheetModal diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 1ffe7fa..1a91811 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -2,6 +2,7 @@ import { Add, Archive, Bolt, + CalendarMonth, CancelRounded, CheckBox, CheckBoxOutlineBlank, @@ -51,12 +52,15 @@ import CompactChoreCard from './CompactChoreCard' import IconButtonWithMenu from './IconButtonWithMenu' import MultiSelectHelp from './MultiSelectHelp' +import { useMediaQuery } from '@mui/material' 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 CalendarDual from '../components/CalendarDual' +import CalendarMonthly from '../components/CalendarMonthly.jsx' import { canScheduleNotification, scheduleChoreNotification, @@ -68,6 +72,7 @@ import SortAndGrouping from './SortAndGrouping' const MyChores = () => { const { data: userProfile, isLoading: isUserProfileLoading } = useUserProfile() + const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md')) const { showSuccess, showError, showWarning } = useNotification() const { impersonatedUser } = useImpersonateUser() const [chores, setChores] = useState([]) @@ -93,9 +98,10 @@ const MyChores = () => { const [searchTerm, setSearchTerm] = useState('') const [performers, setPerformers] = useState([]) const [anchorEl, setAnchorEl] = useState(null) - const [isCompactView, setIsCompactView] = useState( - localStorage.getItem('choreCardViewMode') === 'compact', + const [viewMode, setViewMode] = useState( + localStorage.getItem('choreCardViewMode') || 'default', ) + const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date()) const menuRef = useRef(null) const Navigate = useNavigate() const { data: userLabels, isLoading: userLabelsLoading } = useLabels() @@ -385,17 +391,27 @@ const MyChores = () => { const setSelectedChoreFilterWithCache = value => { setSelectedChoreFilter(value) localStorage.setItem('selectedChoreFilter', value) + // Clear selected calendar date when filters change + setSelectedCalendarDate(null) } const toggleViewMode = () => { - const newMode = !isCompactView - setIsCompactView(newMode) - localStorage.setItem('choreCardViewMode', newMode ? 'compact' : 'default') + const modes = ['default', 'compact', 'calendar'] + const currentIndex = modes.indexOf(viewMode) + const nextIndex = (currentIndex + 1) % modes.length + const newMode = modes[nextIndex] + setViewMode(newMode) + localStorage.setItem('choreCardViewMode', newMode) + + // Clear selected calendar date when switching away from calendar view + if (newMode !== 'calendar') { + setSelectedCalendarDate(null) + } } // Helper function to render the appropriate card component const renderChoreCard = (chore, key) => { - const CardComponent = isCompactView ? CompactChoreCard : ChoreCard + const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard return ( { ) } + // Helper function to get filtered chores for display + const getFilteredChores = () => { + if (searchTerm?.length > 0 || searchFilter !== 'All') { + return filteredChores + } + return chores.filter(ChoreFilters(userProfile)[selectedChoreFilter]) + } + + // Helper function to get chores for a specific date + const getChoresForDate = date => { + const filteredChores = getFilteredChores() + return filteredChores.filter(chore => { + if (!chore.nextDueDate) return false + const choreDate = new Date(chore.nextDueDate).toLocaleDateString() + const selectedDate = date.toLocaleDateString() + return choreDate === selectedDate + }) + } + const updateChores = newChore => { const newChores = chores newChores.push(newChore) @@ -463,6 +498,8 @@ const MyChores = () => { setFilteredChores(priorityFiltered) setSearchFilter('Priority: ' + priority) } + // Clear selected calendar date when filters change + setSelectedCalendarDate(null) } const handleChoreUpdated = (updatedChore, event) => { @@ -607,18 +644,24 @@ const MyChores = () => { if (search === '') { setFilteredChores(chores) setSearchTerm('') + // Clear selected calendar date when search changes + setSelectedCalendarDate(null) return } const term = search.toLowerCase() setSearchTerm(term) setFilteredChores(fuse.search(term).map(result => result.item)) + // Clear selected calendar date when search changes + setSelectedCalendarDate(null) } const handleSearchClose = () => { setSearchTerm('') setFilteredChores(chores) // remove the focus from the search input: setSearchInputFocus(0) + // Clear selected calendar date when search closes + setSelectedCalendarDate(null) } // Multi-select helper functions @@ -1065,10 +1108,20 @@ const MyChores = () => { }} onClick={toggleViewMode} title={ - isCompactView ? 'Switch to Card View' : 'Switch to Compact View' + viewMode === 'default' + ? 'Switch to Compact View' + : viewMode === 'compact' + ? 'Switch to Calendar View' + : 'Switch to Card View' } > - {isCompactView ? : } + {viewMode === 'default' ? ( + + ) : viewMode === 'compact' ? ( + + ) : ( + + )} {/* Multi-select Toggle Button */} @@ -1579,79 +1632,153 @@ const MyChores = () => { )} {(searchTerm?.length > 0 || searchFilter !== 'All') && + viewMode !== 'calendar' && filteredChores.map(chore => renderChoreCard(chore, `filtered-${chore.id}`), )} - {searchTerm.length === 0 && searchFilter === 'All' && ( - - {choreSections.map((section, index) => { - if (section.content.length === 0) return null - return ( - + {/* Calendar Monthly View */} + + {isLargeScreen ? ( + { + setSelectedCalendarDate(date) }} - expanded={Boolean(openChoreSections[index])} - > - - { - if (openChoreSections[index]) { - const newOpenChoreSections = { - ...openChoreSections, - } - delete newOpenChoreSections[index] - setOpenChoreSectionsWithCache(newOpenChoreSections) - } else { - setOpenChoreSectionsWithCache({ - ...openChoreSections, - [index]: true, - }) - } - }} - endDecorator={ - openChoreSections[index] ? ( - - ) : ( - - ) - } - startDecorator={ - <> - - {section?.content?.length} - - - } - > - {section.name} - - - *']: { - // px: 0.5, - px: 0.5, - // pr: 0, - }, + /> + ) : ( +
+ { + setSelectedCalendarDate(date) }} - > - {section.content?.map(chore => renderChoreCard(chore))} - - - ) - })} - + /> +
+ )} +
+ + {/* Selected Date Tasks */} + {selectedCalendarDate && ( + + + Tasks for {selectedCalendarDate.toLocaleDateString()} + + + {getChoresForDate(selectedCalendarDate).length === 0 ? ( + + No tasks scheduled for this date + + ) : ( + getChoresForDate(selectedCalendarDate).map(chore => ( + toggleChoreSelection(chore.id)} + /> + )) + )} + + + )} + )} + {searchTerm.length === 0 && + searchFilter === 'All' && + viewMode !== 'calendar' && ( + + {choreSections.map((section, index) => { + if (section.content.length === 0) return null + return ( + + + { + if (openChoreSections[index]) { + const newOpenChoreSections = { + ...openChoreSections, + } + delete newOpenChoreSections[index] + setOpenChoreSectionsWithCache(newOpenChoreSections) + } else { + setOpenChoreSectionsWithCache({ + ...openChoreSections, + [index]: true, + }) + } + }} + endDecorator={ + openChoreSections[index] ? ( + + ) : ( + + ) + } + startDecorator={ + <> + + {section?.content?.length} + + + } + > + {section.name} + + + *']: { + // px: 0.5, + px: 0.5, + // pr: 0, + }, + }} + > + {section.content?.map(chore => renderChoreCard(chore))} + + + ) + })} + + )}