unifying the empty state across components (#184)
This commit is contained in:
231
src/components/common/EmptyState.jsx
Normal file
231
src/components/common/EmptyState.jsx
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import { Box, Button, Typography } from '@mui/joy'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single empty/error surface for the app.
|
||||||
|
*
|
||||||
|
* variant drives the tone, the icon tile color and the a11y role:
|
||||||
|
* - 'empty' nothing exists yet. Teach the feature, offer the way in.
|
||||||
|
* - 'no-results' something exists, the current search/filter hides it.
|
||||||
|
* - 'error' we failed to load. Say what happened, offer a retry.
|
||||||
|
*
|
||||||
|
* Actions are objects instead of nodes so every call site gets the same
|
||||||
|
* button vocabulary (solid primary lead, plain neutral follow).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TONES = {
|
||||||
|
empty: {
|
||||||
|
tileBg: 'primary.softHoverBg',
|
||||||
|
halo: 'primary.softBg',
|
||||||
|
iconColor: 'primary.softColor',
|
||||||
|
role: 'status',
|
||||||
|
},
|
||||||
|
'no-results': {
|
||||||
|
tileBg: 'neutral.softHoverBg',
|
||||||
|
halo: 'neutral.softBg',
|
||||||
|
iconColor: 'neutral.softColor',
|
||||||
|
role: 'status',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
tileBg: 'danger.softHoverBg',
|
||||||
|
halo: 'danger.softBg',
|
||||||
|
iconColor: 'danger.softColor',
|
||||||
|
role: 'alert',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZES = {
|
||||||
|
sm: {
|
||||||
|
tile: 48,
|
||||||
|
icon: '1.375rem',
|
||||||
|
halo: 6,
|
||||||
|
py: 5,
|
||||||
|
title: 'title-sm',
|
||||||
|
titleSize: '1rem',
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
tile: 68,
|
||||||
|
icon: '1.875rem',
|
||||||
|
halo: 10,
|
||||||
|
py: 8,
|
||||||
|
title: 'title-md',
|
||||||
|
titleSize: '1.25rem',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionButton = ({ action, ...buttonProps }) => {
|
||||||
|
const { label, to, onClick, ...rest } = action
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...buttonProps}
|
||||||
|
{...rest}
|
||||||
|
onClick={onClick}
|
||||||
|
{...(to ? { component: Link, to } : {})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionButton.propTypes = {
|
||||||
|
action: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyState = ({
|
||||||
|
variant = 'empty',
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
primaryAction,
|
||||||
|
secondaryAction,
|
||||||
|
size = 'md',
|
||||||
|
fullHeight = false,
|
||||||
|
sx,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const tone = TONES[variant] || TONES.empty
|
||||||
|
const dimensions = SIZES[size] || SIZES.md
|
||||||
|
const buttonSize = size === 'sm' ? 'sm' : 'md'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
role={tone.role}
|
||||||
|
aria-live={variant === 'error' ? 'assertive' : 'polite'}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
gap: 0.75,
|
||||||
|
px: 3,
|
||||||
|
py: dimensions.py,
|
||||||
|
...(fullHeight && { minHeight: '55vh' }),
|
||||||
|
animation: 'dt-empty-state-in 200ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
'@keyframes dt-empty-state-in': {
|
||||||
|
from: { opacity: 0, transform: 'translateY(4px)' },
|
||||||
|
to: { opacity: 1, transform: 'none' },
|
||||||
|
},
|
||||||
|
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<Box
|
||||||
|
aria-hidden='true'
|
||||||
|
sx={{
|
||||||
|
// Two concentric tints: a wide, pale halo with a deeper medallion
|
||||||
|
// inside it, so the icon reads as an object rather than a chip.
|
||||||
|
width: dimensions.tile + dimensions.halo * 2,
|
||||||
|
height: dimensions.tile + dimensions.halo * 2,
|
||||||
|
mb: 1.5,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: tone.halo,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: dimensions.tile,
|
||||||
|
height: dimensions.tile,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: tone.tileBg,
|
||||||
|
color: tone.iconColor,
|
||||||
|
'& > svg': { fontSize: dimensions.icon },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
level={dimensions.title}
|
||||||
|
sx={{
|
||||||
|
color: 'text.primary',
|
||||||
|
fontSize: dimensions.titleSize,
|
||||||
|
fontWeight: 'lg',
|
||||||
|
letterSpacing: '-0.01em',
|
||||||
|
textWrap: 'balance',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<Typography
|
||||||
|
level='body-sm'
|
||||||
|
sx={{
|
||||||
|
color: 'text.secondary',
|
||||||
|
maxWidth: '38ch',
|
||||||
|
lineHeight: 1.55,
|
||||||
|
textWrap: 'pretty',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(primaryAction || secondaryAction) && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1.5,
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{primaryAction && (
|
||||||
|
<ActionButton
|
||||||
|
action={primaryAction}
|
||||||
|
variant='solid'
|
||||||
|
color='primary'
|
||||||
|
size={buttonSize}
|
||||||
|
sx={{ minWidth: size === 'sm' ? 0 : 148 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{secondaryAction && (
|
||||||
|
<ActionButton
|
||||||
|
action={secondaryAction}
|
||||||
|
variant='plain'
|
||||||
|
color='neutral'
|
||||||
|
size={buttonSize}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EmptyState.propTypes = {
|
||||||
|
variant: PropTypes.oneOf(['empty', 'no-results', 'error']),
|
||||||
|
icon: PropTypes.node,
|
||||||
|
title: PropTypes.node.isRequired,
|
||||||
|
description: PropTypes.node,
|
||||||
|
primaryAction: PropTypes.shape({
|
||||||
|
label: PropTypes.node.isRequired,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
to: PropTypes.string,
|
||||||
|
startDecorator: PropTypes.node,
|
||||||
|
}),
|
||||||
|
secondaryAction: PropTypes.shape({
|
||||||
|
label: PropTypes.node.isRequired,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
to: PropTypes.string,
|
||||||
|
startDecorator: PropTypes.node,
|
||||||
|
}),
|
||||||
|
size: PropTypes.oneOf(['sm', 'md']),
|
||||||
|
fullHeight: PropTypes.bool,
|
||||||
|
sx: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmptyState
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Label,
|
Label,
|
||||||
Person,
|
Person,
|
||||||
PriorityHigh,
|
PriorityHigh,
|
||||||
|
SearchOff,
|
||||||
SelectAll,
|
SelectAll,
|
||||||
Unarchive,
|
Unarchive,
|
||||||
ViewAgenda,
|
ViewAgenda,
|
||||||
@@ -27,6 +28,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
|||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import FilterBar from '../../components/common/FilterBar'
|
import FilterBar from '../../components/common/FilterBar'
|
||||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
@@ -1004,45 +1006,37 @@ const ArchivedTasks = () => {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{finalChores.length === 0 ? (
|
{finalChores.length === 0 ? (
|
||||||
<Box
|
searchTerm || hasActiveFilters ? (
|
||||||
sx={{
|
<EmptyState
|
||||||
display: 'flex',
|
variant='no-results'
|
||||||
justifyContent: 'center',
|
fullHeight
|
||||||
alignItems: 'center',
|
icon={<SearchOff />}
|
||||||
flexDirection: 'column',
|
title='No archived tasks match'
|
||||||
height: '50vh',
|
description={
|
||||||
}}
|
searchTerm
|
||||||
>
|
? `Nothing in the archive matches "${searchTerm}".`
|
||||||
<Archive sx={{ fontSize: '4rem', mb: 1, color: 'text.tertiary' }} />
|
: 'There are archived tasks, but none fit the filters that are currently on.'
|
||||||
<Typography level='title-md' gutterBottom>
|
}
|
||||||
{searchTerm || hasActiveFilters
|
primaryAction={
|
||||||
? 'No archived tasks found'
|
searchTerm
|
||||||
: 'No archived tasks'}
|
? { label: 'Clear search', onClick: handleSearchClose }
|
||||||
</Typography>
|
: { label: 'Clear filters', onClick: clearAll }
|
||||||
<Typography level='body-sm' color='text.secondary' sx={{ mb: 2 }}>
|
}
|
||||||
{searchTerm || hasActiveFilters
|
secondaryAction={
|
||||||
? 'Try adjusting your search or filters'
|
searchTerm && hasActiveFilters
|
||||||
: 'Archived tasks will appear here when you archive them from the main task list'}
|
? { label: 'Clear filters', onClick: clearAll }
|
||||||
</Typography>
|
: undefined
|
||||||
{(searchTerm || hasActiveFilters) && (
|
}
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
/>
|
||||||
{searchTerm && (
|
) : (
|
||||||
<Button
|
<EmptyState
|
||||||
onClick={handleSearchClose}
|
fullHeight
|
||||||
variant='outlined'
|
icon={<Archive />}
|
||||||
color='neutral'
|
title='Nothing archived'
|
||||||
>
|
description='Archiving hides a task without deleting it. Anything you archive from your task list shows up here, ready to restore.'
|
||||||
Clear search
|
primaryAction={{ label: 'Back to tasks', to: '/chores' }}
|
||||||
</Button>
|
/>
|
||||||
)}
|
)
|
||||||
{hasActiveFilters && (
|
|
||||||
<Button onClick={clearAll} variant='outlined' color='neutral'>
|
|
||||||
Clear filters
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : (
|
) : (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography level='body-sm' color='text.secondary' sx={{ mb: 2 }}>
|
<Typography level='body-sm' color='text.secondary' sx={{ mb: 2 }}>
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ import {
|
|||||||
Add,
|
Add,
|
||||||
Bolt,
|
Bolt,
|
||||||
CalendarMonth,
|
CalendarMonth,
|
||||||
|
CloudOff,
|
||||||
EditCalendar,
|
EditCalendar,
|
||||||
|
SearchOff,
|
||||||
ExpandCircleDown,
|
ExpandCircleDown,
|
||||||
PriorityHigh,
|
PriorityHigh,
|
||||||
Style,
|
Style,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import Logo from '../../Logo'
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionGroup,
|
AccordionGroup,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Chip,
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
@@ -33,6 +33,7 @@ import IconButtonWithMenu from './IconButtonWithMenu'
|
|||||||
|
|
||||||
import { useMediaQuery } from '@mui/material'
|
import { useMediaQuery } from '@mui/material'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||||
import { useFilter } from '../../hooks/useFilter'
|
import { useFilter } from '../../hooks/useFilter'
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
@@ -217,8 +218,7 @@ const MyChores = () => {
|
|||||||
)
|
)
|
||||||
case 'Due Later':
|
case 'Due Later':
|
||||||
return (
|
return (
|
||||||
d !== null &&
|
d !== null && d > new Date(now.getTime() + 24 * 60 * 60 * 1000)
|
||||||
d > new Date(now.getTime() + 24 * 60 * 60 * 1000)
|
|
||||||
)
|
)
|
||||||
case 'No Due Date':
|
case 'No Due Date':
|
||||||
return item.nextDueDate === null
|
return item.nextDueDate === null
|
||||||
@@ -637,7 +637,8 @@ const MyChores = () => {
|
|||||||
selectedChores,
|
selectedChores,
|
||||||
addTaskModalOpen,
|
addTaskModalOpen,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
searchFilter: hasQuickFilters || searchTerm?.length > 0 ? 'filtered' : 'All',
|
searchFilter:
|
||||||
|
hasQuickFilters || searchTerm?.length > 0 ? 'filtered' : 'All',
|
||||||
filteredChores: getFilteredChores,
|
filteredChores: getFilteredChores,
|
||||||
choreSections,
|
choreSections,
|
||||||
openChoreSections,
|
openChoreSections,
|
||||||
@@ -826,10 +827,12 @@ const MyChores = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleViewMode = value => {
|
const toggleViewMode = value => {
|
||||||
const newMode = value ?? (() => {
|
const newMode =
|
||||||
const modes = ['default', 'compact', 'calendar']
|
value ??
|
||||||
return modes[(modes.indexOf(viewMode) + 1) % modes.length]
|
(() => {
|
||||||
})()
|
const modes = ['default', 'compact', 'calendar']
|
||||||
|
return modes[(modes.indexOf(viewMode) + 1) % modes.length]
|
||||||
|
})()
|
||||||
setViewMode(newMode)
|
setViewMode(newMode)
|
||||||
localStorage.setItem('choreCardViewMode', newMode)
|
localStorage.setItem('choreCardViewMode', newMode)
|
||||||
if (newMode !== 'calendar') {
|
if (newMode !== 'calendar') {
|
||||||
@@ -905,9 +908,28 @@ const MyChores = () => {
|
|||||||
[getFilteredChores],
|
[getFilteredChores],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// "Narrowed" means the user actively cut the list down (search, quick
|
||||||
|
// filters, a saved filter). Picking a project is not narrowing: an empty
|
||||||
|
// project is an empty place, not a filtered-away result.
|
||||||
|
const isNarrowed = Boolean(
|
||||||
|
searchTerm?.length > 0 || hasQuickFilters || activeFilterId,
|
||||||
|
)
|
||||||
|
const isCustomProjectSelected = Boolean(
|
||||||
|
selectedProject && selectedProject.id !== 'default',
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearNarrowing = () => {
|
||||||
|
clearQuickFilters()
|
||||||
|
setSearchTerm('')
|
||||||
|
clearActiveFilter()
|
||||||
|
updateFilterUrl(null, null)
|
||||||
|
}
|
||||||
|
|
||||||
const appendChore = (prev, newChore) => {
|
const appendChore = (prev, newChore) => {
|
||||||
let newChores = [...prev, newChore]
|
let newChores = [...prev, newChore]
|
||||||
|
|
||||||
|
|
||||||
if (impersonatedUser) {
|
if (impersonatedUser) {
|
||||||
newChores = newChores.filter(
|
newChores = newChores.filter(
|
||||||
chore => chore.assignedTo === impersonatedUser.userId,
|
chore => chore.assignedTo === impersonatedUser.userId,
|
||||||
@@ -930,40 +952,23 @@ const MyChores = () => {
|
|||||||
if (choresError || membersError) {
|
if (choresError || membersError) {
|
||||||
return (
|
return (
|
||||||
<Container maxWidth='md'>
|
<Container maxWidth='md'>
|
||||||
<Box
|
<EmptyState
|
||||||
sx={{
|
variant='error'
|
||||||
display: 'flex',
|
fullHeight
|
||||||
justifyContent: 'center',
|
icon={<CloudOff />}
|
||||||
alignItems: 'center',
|
title={"Can't reach Donetick"}
|
||||||
flexDirection: 'column',
|
description={
|
||||||
height: '70vh',
|
choresErrorDetails?.message ||
|
||||||
gap: 2,
|
'Your tasks are safe. We just could not load them right now, check your connection and try again.'
|
||||||
}}
|
}
|
||||||
>
|
primaryAction={{
|
||||||
<Box sx={{ mb: 2, opacity: 0.7 }}>
|
label: 'Try again',
|
||||||
<Logo />
|
onClick: () => {
|
||||||
</Box>
|
|
||||||
<Typography level='h4' color='danger'>
|
|
||||||
Unable to communicate with server
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
level='body-md'
|
|
||||||
sx={{ textAlign: 'center', maxWidth: 400 }}
|
|
||||||
>
|
|
||||||
{choresErrorDetails?.message ||
|
|
||||||
'The server is currently unavailable. Please check your connection and try again.'}
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant='solid'
|
|
||||||
color='primary'
|
|
||||||
onClick={() => {
|
|
||||||
refetchChores()
|
refetchChores()
|
||||||
queryClient.invalidateQueries(['circleMembers'])
|
queryClient.invalidateQueries(['circleMembers'])
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
Retry Connection
|
/>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1118,50 +1123,79 @@ const MyChores = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Show "Nothing scheduled" when appropriate based on current view mode */}
|
{/* Empty state. Three different situations, three different messages:
|
||||||
{(searchTerm?.length > 0 || hasQuickFilters || activeFilterId
|
nothing created yet, nothing left after narrowing, or an empty
|
||||||
|
project. Only the middle one is about filters. */}
|
||||||
|
{(isNarrowed
|
||||||
? getFilteredChores.length === 0
|
? getFilteredChores.length === 0
|
||||||
: projectFilteredChores.length === 0) &&
|
: projectFilteredChores.length === 0) &&
|
||||||
// only if not in calendar view:
|
// only if not in calendar view:
|
||||||
viewMode !== 'calendar' && (
|
viewMode !== 'calendar' &&
|
||||||
<Box
|
(chores.length === 0 ? (
|
||||||
sx={{
|
<EmptyState
|
||||||
display: 'flex',
|
variant='empty'
|
||||||
justifyContent: 'center',
|
fullHeight
|
||||||
alignItems: 'center',
|
icon={<EditCalendar />}
|
||||||
flexDirection: 'column',
|
title='No tasks yet'
|
||||||
height: '50vh',
|
description='Create your first task and Donetick keeps track of when it is due, whose turn it is, and what comes next.'
|
||||||
|
primaryAction={{
|
||||||
|
label: 'Create a task',
|
||||||
|
startDecorator: <Add />,
|
||||||
|
onClick: () => setAddTaskModalOpen(true),
|
||||||
}}
|
}}
|
||||||
>
|
secondaryAction={{
|
||||||
<EditCalendar
|
label: 'More options',
|
||||||
sx={{
|
onClick: () => Navigate('/chores/create'),
|
||||||
fontSize: '4rem',
|
}}
|
||||||
// color: 'text.disabled',
|
/>
|
||||||
mb: 1,
|
) : isNarrowed ? (
|
||||||
}}
|
<EmptyState
|
||||||
/>
|
variant='no-results'
|
||||||
<Typography level='title-md' gutterBottom>
|
fullHeight
|
||||||
Nothing scheduled
|
icon={<SearchOff />}
|
||||||
</Typography>
|
title='No tasks match this view'
|
||||||
{chores.length > 0 && (
|
description={
|
||||||
<>
|
searchTerm?.length > 0
|
||||||
<Button
|
? `Nothing matches "${searchTerm}". Try a different search, or clear what is narrowing the list.`
|
||||||
onClick={() => {
|
: 'You have tasks, but none of them fit the filters that are currently on.'
|
||||||
clearQuickFilters()
|
}
|
||||||
setSearchTerm('')
|
primaryAction={{
|
||||||
clearActiveFilter()
|
label:
|
||||||
setSelectedProjectWithCache(null)
|
searchTerm?.length > 0 ? 'Clear search' : 'Clear filters',
|
||||||
updateFilterUrl(null, null)
|
onClick: clearNarrowing,
|
||||||
}}
|
}}
|
||||||
variant='outlined'
|
/>
|
||||||
color='neutral'
|
) : isCustomProjectSelected ? (
|
||||||
>
|
<EmptyState
|
||||||
Reset filters
|
variant='empty'
|
||||||
</Button>
|
fullHeight
|
||||||
</>
|
icon={<EditCalendar />}
|
||||||
)}
|
title={`Nothing in ${selectedProject.name} yet`}
|
||||||
</Box>
|
description='Tasks you add to this project show up here. Your other tasks are still where you left them.'
|
||||||
)}
|
primaryAction={{
|
||||||
|
label: 'Add a task here',
|
||||||
|
startDecorator: <Add />,
|
||||||
|
onClick: () => setAddTaskModalOpen(true),
|
||||||
|
}}
|
||||||
|
secondaryAction={{
|
||||||
|
label: 'See tasks outside projects',
|
||||||
|
onClick: () => setSelectedProjectWithCache(null),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
variant='empty'
|
||||||
|
fullHeight
|
||||||
|
icon={<EditCalendar />}
|
||||||
|
title='No tasks here yet'
|
||||||
|
description='Tasks that do not belong to a project live here. Add one, or switch projects to see what is in them.'
|
||||||
|
primaryAction={{
|
||||||
|
label: 'Create a task',
|
||||||
|
startDecorator: <Add />,
|
||||||
|
onClick: () => setAddTaskModalOpen(true),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
{searchTerm?.length > 0 && viewMode !== 'calendar' && (
|
{searchTerm?.length > 0 && viewMode !== 'calendar' && (
|
||||||
<ChoreListView
|
<ChoreListView
|
||||||
chores={getFilteredChores}
|
chores={getFilteredChores}
|
||||||
@@ -1330,16 +1364,17 @@ const MyChores = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getChoresForDate(selectedCalendarDate).length === 0 ? (
|
{getChoresForDate(selectedCalendarDate).length === 0 ? (
|
||||||
<Typography
|
<EmptyState
|
||||||
level='body-sm'
|
size='sm'
|
||||||
sx={{
|
icon={<EditCalendar />}
|
||||||
textAlign: 'center',
|
title='Nothing scheduled'
|
||||||
py: 2,
|
description='This day is free. Add a task if you want something to land here.'
|
||||||
color: 'text.tertiary',
|
primaryAction={{
|
||||||
|
label: 'Add task',
|
||||||
|
startDecorator: <Add />,
|
||||||
|
onClick: () => setAddTaskModalOpen(true),
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
No tasks scheduled for this date
|
|
||||||
</Typography>
|
|
||||||
) : (
|
) : (
|
||||||
<ChoreListView
|
<ChoreListView
|
||||||
chores={getChoresForDate(selectedCalendarDate)}
|
chores={getChoresForDate(selectedCalendarDate)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BarChart, Person } from '@mui/icons-material'
|
import { BarChart, Person } from '@mui/icons-material'
|
||||||
import { Avatar, Box, Sheet, Typography } from '@mui/joy'
|
import { Avatar, Box, Sheet, Typography } from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import { useCircleMembers } from '../../queries/UserQueries'
|
import { useCircleMembers } from '../../queries/UserQueries'
|
||||||
import { TASK_COLOR } from '../../utils/Colors'
|
import { TASK_COLOR } from '../../utils/Colors'
|
||||||
import { resolvePhotoURL } from '../../utils/Helpers'
|
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||||
@@ -127,10 +128,13 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
|
|||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Person sx={{ fontSize: 48, opacity: 0.3, mb: 1 }} />
|
<EmptyState
|
||||||
<Typography level='body-sm' color='neutral'>
|
variant='no-results'
|
||||||
No assigned tasks found
|
size='sm'
|
||||||
</Typography>
|
icon={<Person />}
|
||||||
|
title='No one has tasks yet'
|
||||||
|
description='Assign a task to someone in your circle and their workload shows up here.'
|
||||||
|
/>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
StarBorder,
|
StarBorder,
|
||||||
Task,
|
Task,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import { useChores } from '../../queries/ChoreQueries'
|
import { useChores } from '../../queries/ChoreQueries'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||||
import { getFilterCount, getFilterOverdueCount } from '../../utils/FilterEngine'
|
import { getFilterCount, getFilterOverdueCount } from '../../utils/FilterEngine'
|
||||||
@@ -417,29 +418,17 @@ const FilterView = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{savedFilters.length === 0 ? (
|
{savedFilters.length === 0 ? (
|
||||||
<Box
|
<EmptyState
|
||||||
sx={{
|
fullHeight
|
||||||
p: 4,
|
icon={<FilterAlt />}
|
||||||
textAlign: 'center',
|
title='No saved filters yet'
|
||||||
|
description='Save a set of conditions once, like "overdue and assigned to me", and jump straight back to it from anywhere.'
|
||||||
|
primaryAction={{
|
||||||
|
label: 'Create a filter',
|
||||||
|
startDecorator: <Add />,
|
||||||
|
onClick: handleAddFilter,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<FilterAlt
|
|
||||||
sx={{
|
|
||||||
fontSize: 48,
|
|
||||||
color: 'neutral.300',
|
|
||||||
mb: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
level='title-lg'
|
|
||||||
sx={{ mb: 1, color: 'text.secondary' }}
|
|
||||||
>
|
|
||||||
No saved filters yet
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body-sm' sx={{ color: 'text.tertiary', mb: 2 }}>
|
|
||||||
Create custom filters to quickly access your most used chore
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
) : (
|
) : (
|
||||||
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
||||||
{savedFilters.map(filter => {
|
{savedFilters.map(filter => {
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ import {
|
|||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy'
|
import { Box, Card, Container, Grid, Sheet, Typography } from '@mui/joy'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import FilterBar from '../../components/common/FilterBar'
|
import FilterBar from '../../components/common/FilterBar'
|
||||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||||
@@ -303,36 +304,14 @@ const ChoreHistory = () => {
|
|||||||
}
|
}
|
||||||
if (!choreHistory.length) {
|
if (!choreHistory.length) {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container maxWidth='md'>
|
||||||
maxWidth='md'
|
<EmptyState
|
||||||
sx={{
|
fullHeight
|
||||||
textAlign: 'center',
|
icon={<EventBusy />}
|
||||||
display: 'flex',
|
title='No history yet'
|
||||||
// make sure the content is centered vertically:
|
description='Every time this task gets completed or skipped, it lands here with who did it and when. Nothing has happened yet.'
|
||||||
alignItems: 'center',
|
primaryAction={{ label: 'Back to tasks', to: '/chores' }}
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '50vh',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EventBusy
|
|
||||||
sx={{
|
|
||||||
fontSize: '6rem',
|
|
||||||
// color: 'text.disabled',
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography level='h3' gutterBottom>
|
|
||||||
No History Yet
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body1'>
|
|
||||||
You haven't completed any tasks. Once you start finishing tasks,
|
|
||||||
they'll show up here.
|
|
||||||
</Typography>
|
|
||||||
<Button variant='soft' sx={{ mt: 2 }}>
|
|
||||||
<Link to='/chores'>Go back to chores</Link>
|
|
||||||
</Button>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -441,27 +420,13 @@ const ChoreHistory = () => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{sortedHistory.length === 0 && activeFilterCount > 0 && (
|
{sortedHistory.length === 0 && activeFilterCount > 0 && (
|
||||||
<Box
|
<EmptyState
|
||||||
sx={{
|
variant='no-results'
|
||||||
textAlign: 'center',
|
icon={<FilterList />}
|
||||||
py: 6,
|
title='No history matches these filters'
|
||||||
display: 'flex',
|
description='There is history here, but none of it fits the filters that are currently on.'
|
||||||
flexDirection: 'column',
|
primaryAction={{ label: 'Clear filters', onClick: clearAll }}
|
||||||
alignItems: 'center',
|
/>
|
||||||
gap: 1.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FilterList sx={{ fontSize: '3rem', color: 'text.tertiary' }} />
|
|
||||||
<Typography level='title-md' sx={{ color: 'text.secondary' }}>
|
|
||||||
No results match your filters
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body-sm' sx={{ color: 'text.tertiary' }}>
|
|
||||||
Try adjusting or clearing the active filters.
|
|
||||||
</Typography>
|
|
||||||
<Button variant='soft' size='sm' onClick={clearAll} sx={{ mt: 0.5 }}>
|
|
||||||
Clear filters
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sortedHistory.length > 0 && (
|
{sortedHistory.length > 0 && (
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
TrailingActions,
|
TrailingActions,
|
||||||
} from '@meauxt/react-swipeable-list'
|
} from '@meauxt/react-swipeable-list'
|
||||||
import '@meauxt/react-swipeable-list/dist/styles.css'
|
import '@meauxt/react-swipeable-list/dist/styles.css'
|
||||||
import { Add, MoreVert } from '@mui/icons-material'
|
import { Add, MoreVert, Style } from '@mui/icons-material'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors'
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors'
|
||||||
@@ -258,19 +259,17 @@ const LabelView = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{userLabels.length === 0 && (
|
{userLabels.length === 0 && (
|
||||||
<Box
|
<EmptyState
|
||||||
sx={{
|
fullHeight
|
||||||
display: 'flex',
|
icon={<Style />}
|
||||||
justifyContent: 'center',
|
title='No labels yet'
|
||||||
alignItems: 'center',
|
description='Labels group tasks across your circle, like "kitchen" or "bills", so you can filter down to them in one tap.'
|
||||||
flexDirection: 'column',
|
primaryAction={{
|
||||||
height: '50vh',
|
label: 'Create a label',
|
||||||
|
startDecorator: <Add />,
|
||||||
|
onClick: handleAddLabel,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Typography level='title-md' gutterBottom>
|
|
||||||
No labels available. Add a new label to get started.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
||||||
{userLabels.map(label => (
|
{userLabels.map(label => (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import EmptyState from '../../../components/common/EmptyState'
|
||||||
import ModalActions from '../../../components/common/ModalActions'
|
import ModalActions from '../../../components/common/ModalActions'
|
||||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||||
import { GetChoreAttachments } from '../../../utils/Fetcher'
|
import { GetChoreAttachments } from '../../../utils/Fetcher'
|
||||||
@@ -95,12 +96,12 @@ function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
|||||||
<CircularProgress size='md' />
|
<CircularProgress size='md' />
|
||||||
</Box>
|
</Box>
|
||||||
) : attachments.length === 0 ? (
|
) : attachments.length === 0 ? (
|
||||||
<Typography
|
<EmptyState
|
||||||
level='body-sm'
|
size='sm'
|
||||||
sx={{ color: 'text.secondary', py: 2, textAlign: 'center' }}
|
icon={<AttachFile />}
|
||||||
>
|
title='No attachments'
|
||||||
No attachments found.
|
description='Photos and files added to this task will show up here.'
|
||||||
</Typography>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<List sx={{ '--ListItem-paddingX': '0px' }}>
|
<List sx={{ '--ListItem-paddingX': '0px' }}>
|
||||||
{attachments.map((attachment, index) => (
|
{attachments.map((attachment, index) => (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Analytics,
|
Analytics,
|
||||||
BarChart,
|
BarChart,
|
||||||
CallReceived,
|
CallReceived,
|
||||||
|
CloudOff,
|
||||||
EventBusy,
|
EventBusy,
|
||||||
Schedule,
|
Schedule,
|
||||||
Speed,
|
Speed,
|
||||||
@@ -25,7 +26,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useTheme } from '@mui/joy/styles'
|
import { useTheme } from '@mui/joy/styles'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import {
|
import {
|
||||||
Line,
|
Line,
|
||||||
@@ -35,6 +36,7 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts'
|
} from 'recharts'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import { useThingHistory } from '../../queries/ThingQueries'
|
import { useThingHistory } from '../../queries/ThingQueries'
|
||||||
import LoadingComponent from '../components/Loading'
|
import LoadingComponent from '../components/Loading'
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ const ThingsHistory = () => {
|
|||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
|
refetch,
|
||||||
} = useThingHistory(id)
|
} = useThingHistory(id)
|
||||||
|
|
||||||
// Flatten all pages of history data
|
// Flatten all pages of history data
|
||||||
@@ -152,35 +155,23 @@ const ThingsHistory = () => {
|
|||||||
|
|
||||||
if (error || !thingsHistory || thingsHistory.length === 0) {
|
if (error || !thingsHistory || thingsHistory.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container maxWidth='md'>
|
||||||
maxWidth='md'
|
<EmptyState
|
||||||
sx={{
|
variant={error ? 'error' : 'empty'}
|
||||||
textAlign: 'center',
|
fullHeight
|
||||||
display: 'flex',
|
icon={error ? <CloudOff /> : <EventBusy />}
|
||||||
// make sure the content is centered vertically:
|
title={error ? "Couldn't load this history" : 'No history yet'}
|
||||||
alignItems: 'center',
|
description={
|
||||||
justifyContent: 'center',
|
error
|
||||||
flexDirection: 'column',
|
? 'We could not reach the server. Check your connection and try again.'
|
||||||
height: '50vh',
|
: "Each time this thing's value changes, the change is recorded here."
|
||||||
}}
|
}
|
||||||
>
|
primaryAction={
|
||||||
<EventBusy
|
error
|
||||||
sx={{
|
? { label: 'Try again', onClick: () => refetch() }
|
||||||
fontSize: '6rem',
|
: { label: 'Back to things', to: '/things' }
|
||||||
// color: 'text.disabled',
|
}
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography level='h3' gutterBottom>
|
|
||||||
No history found
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body1'>
|
|
||||||
It looks like there is no history for this thing yet.
|
|
||||||
</Typography>
|
|
||||||
<Button variant='soft' sx={{ mt: 2 }}>
|
|
||||||
<Link to='/things'>Go back to things</Link>
|
|
||||||
</Button>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import {
|
import {
|
||||||
CreateThing,
|
CreateThing,
|
||||||
@@ -405,25 +406,20 @@ const ThingsView = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{things.length === 0 && (
|
{things.length === 0 && (
|
||||||
<Box
|
<EmptyState
|
||||||
sx={{
|
fullHeight
|
||||||
display: 'flex',
|
icon={<Widgets />}
|
||||||
justifyContent: 'center',
|
title='No things yet'
|
||||||
alignItems: 'center',
|
description='A thing tracks a value, like a counter or a switch, that other tasks can react to. Create one to trigger tasks automatically.'
|
||||||
flexDirection: 'column',
|
primaryAction={{
|
||||||
height: '50vh',
|
label: 'Create a thing',
|
||||||
|
startDecorator: <Add />,
|
||||||
|
onClick: () => {
|
||||||
|
setCreateModalThing(null)
|
||||||
|
setIsShowCreateThingModal(true)
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Widgets
|
|
||||||
sx={{
|
|
||||||
fontSize: '4rem',
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography level='title-md' gutterBottom>
|
|
||||||
No things has been created/found
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
||||||
{things.map(thing => (
|
{things.map(thing => (
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import {
|
import {
|
||||||
useChoreTimer,
|
useChoreTimer,
|
||||||
@@ -1204,9 +1205,12 @@ const TimerDetails = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(!timerData.pauseLog || timerData.pauseLog.length === 0) && (
|
{(!timerData.pauseLog || timerData.pauseLog.length === 0) && (
|
||||||
<Alert color='neutral'>
|
<EmptyState
|
||||||
No work sessions found for this timer.
|
size='sm'
|
||||||
</Alert>
|
icon={<AccessTime />}
|
||||||
|
title='No work sessions yet'
|
||||||
|
description='Start the timer on this task and each session lands here.'
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -20,17 +20,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Chip,
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
Grid,
|
Grid,
|
||||||
Link,
|
|
||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import FilterBar from '../../components/common/FilterBar'
|
import FilterBar from '../../components/common/FilterBar'
|
||||||
import { useFilter } from '../../hooks/useFilter'
|
import { useFilter } from '../../hooks/useFilter'
|
||||||
|
|
||||||
@@ -992,54 +991,21 @@ const UserActivites = () => {
|
|||||||
|
|
||||||
{/* Conditional Content Based on Data Availability */}
|
{/* Conditional Content Based on Data Availability */}
|
||||||
{!choresData.res?.length > 0 || !choresHistory?.length > 0 ? (
|
{!choresData.res?.length > 0 || !choresHistory?.length > 0 ? (
|
||||||
<Container
|
<EmptyState
|
||||||
maxWidth='md'
|
variant='no-results'
|
||||||
sx={{
|
fullHeight
|
||||||
textAlign: 'center',
|
icon={<EventBusy />}
|
||||||
display: 'flex',
|
title='No activity in this range'
|
||||||
alignItems: 'center',
|
description={`Nothing was completed by ${
|
||||||
justifyContent: 'center',
|
selectedUser === undefined || selectedUser === 'all'
|
||||||
flexDirection: 'column',
|
? 'anyone in your circle'
|
||||||
height: '50vh',
|
: circleUsers.find(user => user.userId === selectedUser)
|
||||||
}}
|
?.displayName || 'this member'
|
||||||
>
|
} ${
|
||||||
<EventBusy
|
tabValue === 365 ? 'so far' : `in the last ${tabValue} days`
|
||||||
sx={{
|
}. Try a wider time range or a different member.`}
|
||||||
fontSize: '6rem',
|
primaryAction={{ label: 'Back to tasks', to: '/chores' }}
|
||||||
mb: 1,
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Typography level='h3' gutterBottom>
|
|
||||||
No activities found
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body1' sx={{ mb: 1 }}>
|
|
||||||
No activities found for{' '}
|
|
||||||
<Typography
|
|
||||||
component='span'
|
|
||||||
sx={{ fontWeight: 600, color: 'primary.500' }}
|
|
||||||
>
|
|
||||||
{selectedUser === undefined || selectedUser === 'all'
|
|
||||||
? 'All Users'
|
|
||||||
: circleUsers.find(user => user.userId === selectedUser)
|
|
||||||
?.displayName || 'Unknown User'}
|
|
||||||
</Typography>{' '}
|
|
||||||
in the{' '}
|
|
||||||
<Typography
|
|
||||||
component='span'
|
|
||||||
sx={{ fontWeight: 600, color: 'primary.500' }}
|
|
||||||
>
|
|
||||||
{tabValue === 365 ? 'All Time' : `Last ${tabValue} Days`}
|
|
||||||
</Typography>
|
|
||||||
.
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body-sm' sx={{ color: 'text.secondary', mb: 2 }}>
|
|
||||||
Try selecting a different time period or user filter above.
|
|
||||||
</Typography>
|
|
||||||
<Button variant='soft' sx={{ mt: 2 }}>
|
|
||||||
<Link to='/chores'>Go back to chores</Link>
|
|
||||||
</Button>
|
|
||||||
</Container>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Main Content Area - Mobile: Stack vertically, Desktop: Side by side */}
|
{/* Main Content Area - Mobile: Stack vertically, Desktop: Side by side */}
|
||||||
|
|||||||
@@ -1,61 +1,23 @@
|
|||||||
import { HomeRounded, Login } from '@mui/icons-material'
|
import { Explore, HomeRounded } from '@mui/icons-material'
|
||||||
import { Box, Button, CircularProgress, Container } from '@mui/joy'
|
import { Container } from '@mui/joy'
|
||||||
import { Typography } from '@mui/material'
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
import { Link } from 'react-router-dom' // Assuming you are using React Router
|
|
||||||
import Logo from '../../Logo'
|
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound = () => {
|
||||||
return (
|
return (
|
||||||
<Container className='flex h-full items-center justify-center'>
|
<Container maxWidth='sm'>
|
||||||
<Box
|
<EmptyState
|
||||||
className='flex flex-col items-center justify-center'
|
variant='no-results'
|
||||||
sx={{
|
fullHeight
|
||||||
minHeight: '80vh',
|
icon={<Explore />}
|
||||||
|
title='Page not found'
|
||||||
|
description='This link does not lead anywhere. It may have moved, or the address has a typo in it.'
|
||||||
|
primaryAction={{
|
||||||
|
label: 'Go to my tasks',
|
||||||
|
to: '/chores',
|
||||||
|
startDecorator: <HomeRounded />,
|
||||||
}}
|
}}
|
||||||
>
|
secondaryAction={{ label: 'Log in', to: '/login' }}
|
||||||
<CircularProgress
|
/>
|
||||||
value={100}
|
|
||||||
color='danger' // Set the color to 'error' for danger color
|
|
||||||
sx={{ '--CircularProgress-size': '200px' }}
|
|
||||||
>
|
|
||||||
<Logo />
|
|
||||||
</CircularProgress>
|
|
||||||
<Box
|
|
||||||
className='flex items-center gap-2'
|
|
||||||
sx={{
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: 24,
|
|
||||||
mt: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Page Not Found
|
|
||||||
</Box>
|
|
||||||
<Typography level='h2' fontWeight={500} textAlign={'center'}>
|
|
||||||
Sorry, I could be wrong but I think you are lost.
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to='/chores'
|
|
||||||
variant='outlined'
|
|
||||||
color='primary'
|
|
||||||
sx={{ mt: 4 }}
|
|
||||||
size='lg'
|
|
||||||
startDecorator={<HomeRounded />}
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to='/login'
|
|
||||||
variant='outlined'
|
|
||||||
color='primary'
|
|
||||||
sx={{ mt: 1 }}
|
|
||||||
size='lg'
|
|
||||||
startDecorator={<Login />}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user