Merge branch 'dev'

This commit is contained in:
Mo Tarbin
2025-07-13 00:50:48 -04:00
13 changed files with 1030 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import { useCallback, useEffect } from 'react'
import { Outlet, useNavigate } from 'react-router-dom' import { Outlet, useNavigate } from 'react-router-dom'
import { useRegisterSW } from 'virtual:pwa-register/react' import { useRegisterSW } from 'virtual:pwa-register/react'
import { registerCapacitorListeners } from './CapacitorListener' import { registerCapacitorListeners } from './CapacitorListener'
import PageTransition from './components/animations/PageTransition'
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext' import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
import { AuthenticationProvider } from './service/AuthenticationService' import { AuthenticationProvider } from './service/AuthenticationService'
import { useNotification } from './service/NotificationProvider' import { useNotification } from './service/NotificationProvider'
@@ -74,7 +75,9 @@ const AppContent = () => {
<> <>
<ImpersonateUserProvider> <ImpersonateUserProvider>
<NavBar /> <NavBar />
<Outlet /> <PageTransition>
<Outlet />
</PageTransition>
</ImpersonateUserProvider> </ImpersonateUserProvider>
</> </>
) )

View File

@@ -0,0 +1,88 @@
import React from 'react'
import { Box } from '@mui/joy'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import { useStaggeredAnimation, useReducedMotion } from '../../hooks/useAnimations'
import './PageTransition.css'
const AnimatedList = ({
children,
staggerDelay = 50,
animationType = 'stagger', // 'stagger', 'fade', 'slide'
direction = 'up', // 'up', 'down', 'left', 'right'
renderItem,
keyExtractor,
items,
...boxProps
}) => {
// Handle both children and items patterns
let childrenArray
if (items && renderItem) {
childrenArray = items.map((item, index) =>
React.cloneElement(renderItem(item, index), {
key: keyExtractor ? keyExtractor(item, index) : index
})
)
} else {
childrenArray = React.Children.toArray(children)
}
const visibleItems = useStaggeredAnimation(childrenArray.length, staggerDelay)
const prefersReducedMotion = useReducedMotion()
// If user prefers reduced motion, render without animations
if (prefersReducedMotion) {
return (
<Box {...boxProps}>
{items && renderItem ? childrenArray : children}
</Box>
)
}
const getAnimationClass = () => {
switch (animationType) {
case 'fade':
return 'fade'
case 'slide':
return direction === 'up' ? 'slide-up' : 'page'
case 'stagger':
default:
return 'stagger'
}
}
return (
<Box {...boxProps}>
<TransitionGroup component={null}>
{childrenArray.map((child, index) => {
const isVisible = visibleItems.has(index)
return (
<CSSTransition
key={child.key || index}
in={isVisible}
timeout={{
enter: 300,
exit: 200,
}}
classNames={getAnimationClass()}
unmountOnExit={false}
>
<Box
sx={{
opacity: isVisible ? 1 : 0,
transform: isVisible ? 'none' : 'translateY(20px)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
transitionDelay: `${index * staggerDelay}ms`,
}}
>
{child}
</Box>
</CSSTransition>
)
})}
</TransitionGroup>
</Box>
)
}
export default AnimatedList

View File

@@ -0,0 +1,105 @@
import { Box, CircularProgress, Typography } from '@mui/joy'
import { styled } from '@mui/joy/styles'
// Styled components with keyframe animations
const LoadingContainer = styled(Box)({
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(8px)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999,
animation: 'fadeIn 0.3s ease-out',
'@keyframes fadeIn': {
from: {
opacity: 0,
},
to: {
opacity: 1,
},
},
})
const LoadingContent = styled(Box)({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '24px',
animation: 'slideUp 0.5s ease-out 0.2s both',
'@keyframes slideUp': {
from: {
opacity: 0,
transform: 'translateY(20px)',
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
},
})
const PulsingText = styled(Typography)({
animation: 'pulse 2s ease-in-out infinite',
'@keyframes pulse': {
'0%, 100%': {
opacity: 1,
},
'50%': {
opacity: 0.5,
},
},
})
const LogoContainer = styled(Box)({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginBottom: '24px',
})
const LoadingScreen = ({
message = 'Loading...',
showLogo = true,
size = 'lg',
}) => {
return (
<LoadingContainer>
<LoadingContent>
{showLogo && (
<LogoContainer>
<Typography
level='h1'
sx={{
fontSize: { xs: '2rem', sm: '2.5rem' },
fontWeight: 700,
color: 'primary.500',
mb: 1,
}}
>
Done
<span style={{ color: '#06b6d4' }}>tick</span>
</Typography>
</LogoContainer>
)}
<CircularProgress
size={size}
sx={{
'--CircularProgress-size': size === 'lg' ? '60px' : '40px',
mb: 2,
}}
/>
<PulsingText level='body-md'>{message}</PulsingText>
</LoadingContent>
</LoadingContainer>
)
}
export default LoadingScreen

View File

@@ -0,0 +1,208 @@
/* Page Transition Animations */
.page-wrapper {
position: relative;
width: 100%;
min-height: 100vh;
}
/* Enter animations */
.page-enter {
opacity: 0;
transform: translateX(20px);
}
.page-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
/* Exit animations */
.page-exit {
opacity: 1;
transform: translateX(0);
}
.page-exit-active {
opacity: 0;
transform: translateX(-20px);
transition: opacity 200ms ease-in, transform 200ms ease-in;
}
/* Specific animation for back navigation */
.page-back-enter {
opacity: 0;
transform: translateX(-20px);
}
.page-back-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
.page-back-exit {
opacity: 1;
transform: translateX(0);
}
.page-back-exit-active {
opacity: 0;
transform: translateX(20px);
transition: opacity 200ms ease-in, transform 200ms ease-in;
}
/* Modal/overlay animations */
.modal-enter {
opacity: 0;
transform: scale(0.95) translateY(10px);
}
.modal-enter-active {
opacity: 1;
transform: scale(1) translateY(0);
transition: opacity 250ms ease-out, transform 250ms ease-out;
}
.modal-exit {
opacity: 1;
transform: scale(1) translateY(0);
}
.modal-exit-active {
opacity: 0;
transform: scale(0.95) translateY(10px);
transition: opacity 200ms ease-in, transform 200ms ease-in;
}
/* Fade animation for simple transitions */
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: opacity 250ms ease-out;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: opacity 200ms ease-in;
}
/* Slide up animation for bottom sheets/panels */
.slide-up-enter {
opacity: 0;
transform: translateY(100%);
}
.slide-up-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
.slide-up-exit {
opacity: 1;
transform: translateY(0);
}
.slide-up-exit-active {
opacity: 0;
transform: translateY(100%);
transition: opacity 250ms ease-in, transform 250ms ease-in;
}
/* Loading spinner animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-spinner {
animation: spin 1s linear infinite;
}
/* Skeleton loading animations */
@keyframes skeleton-loading {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200px 100%;
animation: skeleton-loading 1.5s infinite;
}
/* Micro-interactions */
.interactive-element {
transition: all 0.2s ease-out;
transform: translateZ(0); /* Enable GPU acceleration */
}
.interactive-element:hover {
transform: translateY(-2px) translateZ(0);
}
.interactive-element:active {
transform: translateY(0) translateZ(0);
transition: all 0.1s ease-out;
}
/* Stagger animation for lists */
.stagger-enter {
opacity: 0;
transform: translateY(20px);
}
.stagger-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
/* Add animation delays for staggered effects */
.stagger-enter-active:nth-child(1) { transition-delay: 0ms; }
.stagger-enter-active:nth-child(2) { transition-delay: 50ms; }
.stagger-enter-active:nth-child(3) { transition-delay: 100ms; }
.stagger-enter-active:nth-child(4) { transition-delay: 150ms; }
.stagger-enter-active:nth-child(5) { transition-delay: 200ms; }
.stagger-enter-active:nth-child(6) { transition-delay: 250ms; }
.stagger-enter-active:nth-child(7) { transition-delay: 300ms; }
.stagger-enter-active:nth-child(8) { transition-delay: 350ms; }
/* Reduced motion for accessibility */
@media (prefers-reduced-motion: reduce) {
.page-enter,
.page-enter-active,
.page-exit,
.page-exit-active,
.modal-enter,
.modal-enter-active,
.modal-exit,
.modal-exit-active,
.fade-enter,
.fade-enter-active,
.fade-exit,
.fade-exit-active,
.interactive-element,
.stagger-enter,
.stagger-enter-active {
transition: none !important;
animation: none !important;
transform: none !important;
}
}

View File

@@ -0,0 +1,88 @@
import React, { useEffect, useRef } from 'react'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import { useLocation } from 'react-router-dom'
import './PageTransition.css'
// Route hierarchy for determining navigation direction
const routeHierarchy = {
'/': 0,
'/my/chores': 0,
'/chores': 1,
'/chores/create': 2,
'/settings': 1,
'/things': 1,
'/activities': 1,
'/points': 1,
'/labels': 1,
'/login': 0,
'/signup': 1,
'/landing': 0,
}
const getRouteLevel = pathname => {
// Check for exact matches first
if (routeHierarchy[pathname] !== undefined) {
return routeHierarchy[pathname]
}
// Check for dynamic routes (e.g., /chores/123/edit)
if (pathname.includes('/chores/') && pathname.includes('/edit')) {
return 3
}
if (pathname.includes('/chores/') && pathname.includes('/history')) {
return 3
}
if (pathname.includes('/chores/') && !pathname.includes('/edit') && !pathname.includes('/history')) {
return 2
}
if (pathname.includes('/things/')) {
return 2
}
// Default level
return 1
}
const PageTransition = ({ children }) => {
const location = useLocation()
const prevLocation = useRef(location)
const isNavigatingBack = useRef(false)
useEffect(() => {
const currentLevel = getRouteLevel(location.pathname)
const previousLevel = getRouteLevel(prevLocation.current.pathname)
// Determine if we're navigating back (to a higher level in hierarchy)
isNavigatingBack.current = currentLevel < previousLevel
prevLocation.current = location
}, [location])
const getTransitionClasses = () => {
if (location.pathname.includes('/login') || location.pathname.includes('/signup') || location.pathname.includes('/landing')) {
return 'fade' // Use fade for auth pages
}
return isNavigatingBack.current ? 'page-back' : 'page'
}
return (
<TransitionGroup component={null}>
<CSSTransition
key={location.pathname}
classNames={getTransitionClasses()}
timeout={{
enter: 300,
exit: 200,
}}
unmountOnExit
>
<div className="page-wrapper">
{children}
</div>
</CSSTransition>
</TransitionGroup>
)
}
export default PageTransition

View File

@@ -0,0 +1,138 @@
import { Box, Skeleton } from '@mui/joy'
const SkeletonLoader = ({
type = 'card',
count = 1,
height = 100,
width = '100%',
variant = 'rectangular',
...props
}) => {
const renderSkeleton = () => {
switch (type) {
case 'card':
return (
<Box
sx={{
p: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 'md',
}}
>
<Skeleton variant='text' height={24} width='60%' sx={{ mb: 1 }} />
<Skeleton
variant='text'
height={16}
width='100%'
sx={{ mb: 0.5 }}
/>
<Skeleton variant='text' height={16} width='80%' sx={{ mb: 2 }} />
<Box sx={{ display: 'flex', gap: 1 }}>
<Skeleton variant='circular' width={32} height={32} />
<Skeleton variant='rectangular' height={32} width={80} />
</Box>
</Box>
)
case 'list':
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 1.5 }}>
<Skeleton variant='circular' width={40} height={40} />
<Box sx={{ flex: 1 }}>
<Skeleton
variant='text'
height={20}
width='70%'
sx={{ mb: 0.5 }}
/>
<Skeleton variant='text' height={16} width='50%' />
</Box>
<Skeleton variant='rectangular' width={60} height={24} />
</Box>
)
case 'chore':
return (
<Box
sx={{
p: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 'md',
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
mb: 1,
}}
>
<Skeleton variant='text' height={24} width='50%' />
<Skeleton variant='circular' width={24} height={24} />
</Box>
<Skeleton variant='text' height={16} width='80%' sx={{ mb: 1 }} />
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<Skeleton variant='rectangular' height={20} width={60} />
<Skeleton variant='rectangular' height={20} width={40} />
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Skeleton variant='circular' width={32} height={32} />
<Skeleton variant='text' height={16} width='30%' />
</Box>
</Box>
)
case 'profile':
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
p: 3,
}}
>
<Skeleton
variant='circular'
width={80}
height={80}
sx={{ mb: 2 }}
/>
<Skeleton variant='text' height={24} width={150} sx={{ mb: 1 }} />
<Skeleton variant='text' height={16} width={100} />
</Box>
)
default:
return (
<Skeleton
variant={variant}
height={height}
width={width}
{...props}
/>
)
}
}
return (
<>
{Array.from({ length: count }, (_, index) => (
<Box key={index} sx={{ mb: type === 'list' ? 0 : 2 }}>
{renderSkeleton()}
</Box>
))}
</>
)
}
export default SkeletonLoader

View File

@@ -0,0 +1,107 @@
import { Button } from '@mui/joy'
import { styled } from '@mui/joy/styles'
const AnimatedButton = styled(Button)(({ theme }) => ({
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
transform: 'translateZ(0)', // Enable GPU acceleration
position: 'relative',
overflow: 'hidden',
'&:hover:not(:disabled)': {
transform: 'translateY(-2px) translateZ(0)',
boxShadow: theme.shadow.lg,
},
'&:active:not(:disabled)': {
transform: 'translateY(0) translateZ(0)',
transition: 'all 0.1s cubic-bezier(0.4, 0, 0.2, 1)',
},
'&:focus-visible': {
outline: '2px solid',
outlineColor: theme.palette.primary[500],
outlineOffset: '2px',
},
// Ripple effect
'&::before': {
content: '""',
position: 'absolute',
top: '50%',
left: '50%',
width: '0',
height: '0',
borderRadius: '50%',
background: 'currentColor',
opacity: 0.1,
transform: 'translate(-50%, -50%)',
transition: 'width 0.6s, height 0.6s',
},
'&:active::before': {
width: '300px',
height: '300px',
},
// Loading state
'&[data-loading="true"]': {
pointerEvents: 'none',
position: 'relative',
'& > *': {
opacity: 0.6,
},
'&::after': {
content: '""',
position: 'absolute',
top: '50%',
left: '50%',
width: '16px',
height: '16px',
border: '2px solid currentColor',
borderTop: '2px solid transparent',
borderRadius: '50%',
transform: 'translate(-50%, -50%)',
animation: 'spin 1s linear infinite',
},
},
// Reduced motion support
'@media (prefers-reduced-motion: reduce)': {
transition: 'none',
transform: 'none !important',
'&:hover:not(:disabled)': {
transform: 'none',
},
'&:active:not(:disabled)': {
transform: 'none',
},
'&::before': {
display: 'none',
},
},
}))
const SmoothButton = ({ children, loading = false, onClick, ...props }) => {
const handleClick = event => {
if (loading) return
onClick?.(event)
}
return (
<AnimatedButton
{...props}
onClick={handleClick}
data-loading={loading}
disabled={props.disabled || loading}
>
{children}
</AnimatedButton>
)
}
export default SmoothButton

View File

@@ -0,0 +1,88 @@
import React from 'react'
import { Card } from '@mui/joy'
import { styled } from '@mui/joy/styles'
const AnimatedCard = styled(Card)(({ theme }) => ({
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
transform: 'translateZ(0)', // Enable GPU acceleration
cursor: 'pointer',
position: 'relative',
'&:hover': {
transform: 'translateY(-4px) translateZ(0)',
boxShadow: theme.shadow.lg,
},
'&:active': {
transform: 'translateY(-2px) translateZ(0)',
transition: 'all 0.1s cubic-bezier(0.4, 0, 0.2, 1)',
},
// Subtle background animation on hover
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent)',
opacity: 0,
transition: 'opacity 0.3s ease',
pointerEvents: 'none',
borderRadius: 'inherit',
},
'&:hover::before': {
opacity: 1,
},
// Focus states for accessibility
'&:focus-visible': {
outline: '2px solid',
outlineColor: theme.palette.primary[500],
outlineOffset: '2px',
},
// Reduced motion support
'@media (prefers-reduced-motion: reduce)': {
transition: 'none',
transform: 'none !important',
'&:hover': {
transform: 'none',
boxShadow: theme.shadow.md, // Still provide visual feedback
},
'&:active': {
transform: 'none',
},
'&::before': {
display: 'none',
},
},
}))
const SmoothCard = ({
children,
onClick,
animationDisabled = false,
...props
}) => {
if (animationDisabled) {
return (
<Card {...props} onClick={onClick}>
{children}
</Card>
)
}
return (
<AnimatedCard {...props} onClick={onClick}>
{children}
</AnimatedCard>
)
}
export default SmoothCard

View File

@@ -0,0 +1,54 @@
import { Box } from '@mui/joy'
import React, { useEffect, useState } from 'react'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import './PageTransition.css'
const StaggeredList = ({
children,
staggerDelay = 50,
initialDelay = 0,
animate = true,
}) => {
const [isVisible, setIsVisible] = useState(!animate)
useEffect(() => {
if (animate) {
const timer = setTimeout(() => {
setIsVisible(true)
}, initialDelay)
return () => clearTimeout(timer)
}
}, [animate, initialDelay])
if (!animate) {
return <Box>{children}</Box>
}
const childrenArray = React.Children.toArray(children)
return (
<Box>
<TransitionGroup component={null}>
{isVisible &&
childrenArray.map((child, index) => (
<CSSTransition
key={child.key || index}
classNames='stagger'
timeout={{
enter: 300 + index * staggerDelay,
exit: 200,
}}
style={{
transitionDelay: `${index * staggerDelay}ms`,
}}
>
<Box sx={{ mb: 1 }}>{child}</Box>
</CSSTransition>
))}
</TransitionGroup>
</Box>
)
}
export default StaggeredList

View File

@@ -0,0 +1,10 @@
// Animation Components
export { default as AnimatedList } from './AnimatedList'
export { default as LoadingScreen } from './LoadingScreen'
export { default as PageTransition } from './PageTransition'
export { default as SmoothButton } from './SmoothButton'
export { default as SmoothCard } from './SmoothCard'
export { default as StaggeredList } from './StaggeredList'
// Animation Styles
import './PageTransition.css'

View File

@@ -0,0 +1,90 @@
import { useEffect, useState } from 'react'
// Hook to detect user's motion preferences
export const useReducedMotion = () => {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
setPrefersReducedMotion(mediaQuery.matches)
const handleChange = (event) => {
setPrefersReducedMotion(event.matches)
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
return prefersReducedMotion
}
// Hook for staggered animations
export const useStaggeredAnimation = (itemCount, delay = 50) => {
const [visibleItems, setVisibleItems] = useState(new Set())
const prefersReducedMotion = useReducedMotion()
useEffect(() => {
if (prefersReducedMotion) {
// Show all items immediately if reduced motion is preferred
setVisibleItems(new Set(Array.from({ length: itemCount }, (_, i) => i)))
return
}
const timeouts = []
// Stagger the appearance of items
for (let i = 0; i < itemCount; i++) {
const timeout = setTimeout(() => {
setVisibleItems(prev => new Set([...prev, i]))
}, i * delay)
timeouts.push(timeout)
}
return () => {
timeouts.forEach(clearTimeout)
}
}, [itemCount, delay, prefersReducedMotion])
return visibleItems
}
// Hook for intersection observer animations
export const useInViewAnimation = (threshold = 0.1) => {
const [isInView, setIsInView] = useState(false)
const [element, setElement] = useState(null)
useEffect(() => {
if (!element) return
const observer = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting)
},
{ threshold }
)
observer.observe(element)
return () => {
observer.unobserve(element)
}
}, [element, threshold])
return [setElement, isInView]
}
// Hook for page transition context
export const usePageTransition = () => {
const [isTransitioning, setIsTransitioning] = useState(false)
const startTransition = () => setIsTransitioning(true)
const endTransition = () => setIsTransitioning(false)
return {
isTransitioning,
startTransition,
endTransition,
}
}

View File

@@ -1,3 +1,51 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Global animations for smooth interactions */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Smooth scrolling for better UX */
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
/* Global focus styles for better accessibility */
*:focus-visible {
outline: 2px solid #0ea5e9;
outline-offset: 2px;
border-radius: 4px;
}
/* Improve button and interactive element performance */
button, a, [role="button"] {
transform: translateZ(0);
backface-visibility: hidden;
}
/* Ensure smooth transitions for dynamic content */
* {
box-sizing: border-box;
}
/* Optimize animations for performance */
.animate-optimized {
will-change: transform, opacity;
}
.animate-optimized.animation-complete {
will-change: auto;
}

View File

@@ -14,13 +14,13 @@ import {
import moment from 'moment' import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import { LoadingScreen, SmoothCard } from '../../components/animations'
import { import {
DeleteChoreHistory, DeleteChoreHistory,
GetAllCircleMembers, GetAllCircleMembers,
GetChoreHistory, GetChoreHistory,
UpdateChoreHistory, UpdateChoreHistory,
} from '../../utils/Fetcher' } from '../../utils/Fetcher'
import LoadingComponent from '../components/Loading'
import EditHistoryModal from '../Modals/EditHistoryModal' import EditHistoryModal from '../Modals/EditHistoryModal'
import HistoryCard from './HistoryCard' import HistoryCard from './HistoryCard'
@@ -143,7 +143,7 @@ const ChoreHistory = () => {
} }
if (isLoading) { if (isLoading) {
return <LoadingComponent /> return <LoadingScreen message='Loading task history...' />
} }
if (!choreHistory.length) { if (!choreHistory.length) {
return ( return (