Files
donetick/src/views/User/UserActivities.jsx
Mo Tarbin c9178db87d Enhance Settings, Things History, User Activities, and User Points views
- Integrated RevenueCat for subscription management in Settings.
- Added subscription and user deletion modals in Settings.
- Improved analytics display in Things History with update frequency and trend calculations.
- Enhanced User Activities timeline with a more structured layout.
- Revamped User Points section with a leaderboard and detailed points analytics.
- Introduced responsive design elements and improved user experience across various components.
- Add permission for Camera to support uploading photo via camera
2025-08-04 22:25:30 -04:00

1167 lines
36 KiB
JavaScript

import CancelIcon from '@mui/icons-material/Cancel'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CircleIcon from '@mui/icons-material/Circle'
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
import { EventBusy, Group, Timeline, Toll } from '@mui/icons-material'
import {
Avatar,
Box,
Button,
Card,
Chip,
Container,
Divider,
Grid,
Link,
Option,
Select,
Stack,
Tab,
TabList,
Tabs,
Typography,
} from '@mui/joy'
import React, { useEffect, useState } from 'react'
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { ChoresGrouper } from '../../utils/Chores'
import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx'
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
import LoadingComponent from '../components/Loading'
const groupByDate = history => {
const aggregated = {}
for (let i = 0; i < history.length; i++) {
const item = history[i]
const date = new Date(item.performedAt).toLocaleDateString()
if (!aggregated[date]) {
aggregated[date] = []
}
aggregated[date].push(item)
}
return aggregated
}
const ChoreHistoryItem = ({ time, name, points, status }) => {
const statusIcon = {
completed: <CheckCircleIcon color='success' />,
missed: <CancelIcon color='error' />,
pending: <CircleIcon color='neutral' />,
}
return (
<Stack direction='row' alignItems='center' spacing={2}>
<Typography level='body-md' sx={{ minWidth: 80 }}>
{time}
</Typography>
<Box>
{statusIcon[status] ? statusIcon[status] : statusIcon['completed']}
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
minHeight: 40,
// center vertically:
justifyContent: 'center',
}}
>
<Typography
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '50vw',
}}
level='body-md'
>
{name}
</Typography>
{points && (
<Chip size='sm' color='success' startDecorator={<Toll />}>
{`${points} points`}
</Chip>
)}
</Box>
</Stack>
)
}
const ChoreHistoryTimeline = ({ history }) => {
const groupedHistory = groupByDate(history)
return (
<Container sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Timeline sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
<Typography level='h4' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
Activities Timeline
</Typography>
</Box>
{Object.entries(groupedHistory).map(([date, items]) => (
<Box key={date} sx={{ mb: 4 }}>
<Typography level='title-sm' sx={{ mb: 0.5 }}>
{new Date(date).toLocaleDateString([], {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</Typography>
<Divider />
<Stack spacing={1}>
{items.map(record => (
<>
<ChoreHistoryItem
key={record.id}
time={new Date(record.performedAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
name={record.choreName}
points={record.points}
status={record.status}
/>
</>
))}
</Stack>
</Box>
))}
</Container>
)
}
const renderPieChart = (data, size, isPrimary, chartType = null) => {
// Filter out items with zero or negative values
const validData = data.filter(item => item.value > 0)
if (validData.length === 0) {
return (
<Box
sx={{
width: size,
height: size,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed',
borderColor: 'divider',
borderRadius: '8px',
}}
>
<Typography level='body-sm' color='neutral'>
No data available
</Typography>
</Box>
)
}
// For primary charts, render chart and legend separately to control layout better
if (isPrimary) {
const chartSize = Math.min(size - 20, 220) // Reserve space and limit max size
return (
<Box
sx={{
width: size,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: validData.length <= 3 ? 1.5 : 2, // Smaller gap for fewer items
}}
>
{/* Chart Container */}
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<PieChart width={chartSize} height={chartSize}>
<Pie
data={validData}
dataKey='value'
nameKey='label'
cx='50%'
cy='50%'
outerRadius={chartSize / 3}
innerRadius={chartSize / 8}
paddingAngle={validData.length > 1 ? 2 : 0}
cornerRadius={3}
minAngle={5}
>
{validData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
formatter={(value, name, props) => {
if (chartType === 'tasksTime' && props.payload.count) {
return [`${value}h (${props.payload.count} times)`, name]
}
return [`${value}`, name]
}}
/>
</PieChart>
</Box>
{/* Scrollable Legend Container */}
<Box
sx={{
width: '100%',
maxHeight: validData.length <= 3 ? 'auto' : '120px', // Dynamic height based on data count
minHeight: validData.length <= 3 ? 'auto' : '60px', // No minimum height for few items
overflowY: validData.length <= 3 ? 'visible' : 'auto', // No scroll for few items
overflowX: 'hidden',
px: 1,
'&::-webkit-scrollbar': {
width: '6px',
},
'&::-webkit-scrollbar-track': {
backgroundColor: 'neutral.100',
borderRadius: '3px',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'neutral.400',
borderRadius: '3px',
'&:hover': {
backgroundColor: 'neutral.500',
},
},
}}
>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 0.5,
justifyContent: 'center',
alignItems: 'flex-start',
}}
>
{validData.map((entry, index) => (
<Chip
key={`legend-${index}`}
size='sm'
variant='soft'
sx={{
// backgroundColor: `${entry.color}20`, // 20% opacity
// borderColor: entry.color,
// border: '1px solid',
color: 'text.primary',
fontSize: '0.7rem',
py: 0.5,
px: 1,
maxWidth: '100%',
'&:hover': {
backgroundColor: `${entry.color}30`, // 30% opacity on hover
},
}}
startDecorator={
<Box
sx={{
width: 8,
height: 8,
backgroundColor: entry.color,
borderRadius: '50%',
flexShrink: 0,
}}
/>
}
>
<Typography
level='body-xs'
sx={{
fontSize: '0.7rem',
fontWeight: 500,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '150px',
}}
title={`${entry.label}: ${entry.value}${
chartType === 'tasksTime' && entry.count
? ` (${entry.count} times)`
: ''
}${
chartType === 'labelsDuration' || chartType === 'tasksTime'
? 'h'
: ''
}`}
>
{entry.label}: {entry.value}
{chartType === 'tasksTime' && entry.count
? ` (${entry.count}x)`
: ''}
{chartType === 'labelsDuration' || chartType === 'tasksTime'
? 'h'
: ''}
</Typography>
</Chip>
))}
</Box>
</Box>
</Box>
)
}
// For small preview charts, keep it simple without legend
return (
<PieChart width={size} height={size}>
<Pie
data={validData}
dataKey='value'
nameKey='label'
cx='50%'
cy='50%'
outerRadius={size / 3.5}
innerRadius={size / 8}
paddingAngle={validData.length > 1 ? 1 : 0}
cornerRadius={2}
>
{validData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
)
}
const USER_FILTER = (history, userId) => {
if (userId === undefined || userId === 'all') return true
return history.completedBy === userId
}
const UserActivites = () => {
const { data: userProfile } = useUserProfile()
const [tabValue, setTabValue] = React.useState(30)
const [selectedHistory, setSelectedHistory] = React.useState([])
const [enrichedHistory, setEnrichedHistory] = React.useState([])
const [selectedChart, setSelectedChart] = React.useState('history')
const [historyPieChartData, setHistoryPieChartData] = React.useState([])
const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([])
const [choresPriorityChartData, setChoresPriorityChartData] = React.useState(
[],
)
const [choresLabelsChartData, setChoresLabelsChartData] = React.useState([])
const [choresLabelsDurationChartData, setChoresLabelsDurationChartData] =
React.useState([])
const [tasksTimeChartData, setTasksTimeChartData] = React.useState([])
const [
choresAssigneeBreakdownChartData,
setChoresAssigneeBreakdownChartData,
] = React.useState([])
const { data: choresData, isLoading: isChoresLoading } = useChores(true)
const {
data: choresHistory,
isChoresHistoryLoading,
handleLimitChange: refetchHistory,
} = useChoresHistory(tabValue ? tabValue : 30, true)
const { data: circleMembersData } = useCircleMembers()
const [selectedUser, setSelectedUser] = React.useState('all')
const [circleUsers, setCircleUsers] = useState([])
useEffect(() => {
if (circleMembersData) {
setCircleUsers(circleMembersData.res)
}
}, [circleMembersData])
useEffect(() => {
if (
!isChoresHistoryLoading &&
!isChoresLoading &&
choresHistory &&
choresData?.res
) {
const enrichedHistory = choresHistory.map(item => {
const chore = choresData.res.find(chore => chore.id === item.choreId)
return {
...item,
choreName: chore?.name,
}
})
setEnrichedHistory(enrichedHistory)
const filteredHistory = enrichedHistory.filter(h =>
USER_FILTER(h, selectedUser),
)
setSelectedHistory(filteredHistory)
setHistoryPieChartData(generateHistoryPieChartData(filteredHistory))
// Generate labels duration chart data when both chores and history are available
setChoresLabelsDurationChartData(
generateChoreLabelsWithDurationChartData(
choresData.res,
filteredHistory,
),
)
// Generate tasks time chart data
setTasksTimeChartData(generateTasksTimeChartData(filteredHistory))
} else {
// Reset data when loading or no data
setEnrichedHistory([])
setSelectedHistory([])
setHistoryPieChartData([])
setChoresLabelsDurationChartData([])
setTasksTimeChartData([])
}
}, [
isChoresHistoryLoading,
isChoresLoading,
choresHistory,
choresData?.res,
selectedUser,
])
useEffect(() => {
if (!isChoresLoading && choresData) {
// Filter chores based on selected user
const filteredChores =
selectedUser === 'all' || selectedUser === undefined
? choresData.res
: choresData.res.filter(chore => chore.assignedTo === selectedUser)
const generateChorePriorityPieChartData = chores => {
const groups = ChoresGrouper('priority', chores, null)
return groups
.map(group => {
return {
label: group.name,
value: group.content.length,
color: group.color,
id: group.name,
}
})
.filter(item => item.value > 0)
}
const generateChoreLabelsChartData = chores => {
const labelCounts = {}
let unlabeledCount = 0
chores.forEach(chore => {
if (chore.labelsV2 && chore.labelsV2.length > 0) {
chore.labelsV2.forEach(label => {
if (labelCounts[label.id]) {
labelCounts[label.id].count++
} else {
labelCounts[label.id] = {
label: label.name,
count: 1,
color: label.color || TASK_COLOR.ANYTIME,
id: label.id,
}
}
})
} else {
unlabeledCount++
}
})
const result = Object.values(labelCounts)
.map(item => ({
label: item.label,
value: item.count,
color: item.color,
id: item.id,
}))
.filter(item => item.value > 0)
.sort((a, b) => b.value - a.value) // Sort by count descending
// Add unlabeled tasks if there are any
if (unlabeledCount > 0) {
result.push({
label: 'No Labels',
value: unlabeledCount,
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
})
}
return result
}
const generateChoreAssigneeBreakdownChartData = chores => {
const assigneeCounts = {}
// Define a set of distinct colors for different assignees
const assigneeColors = Object.values(COLORS)
let colorIndex = 0
chores.forEach(chore => {
const assignee = circleUsers.find(
user => user.userId === chore.assignedTo,
)
const assigneeName = assignee ? assignee.displayName : 'Unassigned'
const assigneeId = chore.assignedTo || 'unassigned'
if (assigneeCounts[assigneeId]) {
assigneeCounts[assigneeId].count++
} else {
assigneeCounts[assigneeId] = {
label: assigneeName,
count: 1,
color:
assigneeId === 'unassigned'
? TASK_COLOR.ANYTIME
: assigneeColors[colorIndex % assigneeColors.length],
id: assigneeId,
}
if (assigneeId !== 'unassigned') {
colorIndex++
}
}
})
return Object.values(assigneeCounts)
.map(item => ({
label: item.label,
value: item.count,
color: item.color,
id: item.id,
}))
.filter(item => item.value > 0)
.sort((a, b) => b.value - a.value) // Sort by count descending
}
const choreDuePieChartData = generateChoreDuePieChartData(filteredChores)
setChoreDuePieChartData(choreDuePieChartData)
setChoresPriorityChartData(
generateChorePriorityPieChartData(filteredChores),
)
setChoresLabelsChartData(generateChoreLabelsChartData(filteredChores))
setChoresAssigneeBreakdownChartData(
generateChoreAssigneeBreakdownChartData(filteredChores),
)
}
}, [isChoresLoading, choresData, userProfile?.id, circleUsers, selectedUser])
const generateChoreLabelsWithDurationChartData = (chores, history) => {
if (!chores || !history || chores.length === 0 || history.length === 0) {
return []
}
const labelDurations = {}
let unlabeledDuration = 0
// Iterate through ChoreHistory to get actual time spent
history.forEach(historyItem => {
const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory
// Find the corresponding chore to get its labels
const chore = chores.find(c => c.id === historyItem.choreId)
if (chore && chore.labelsV2 && chore.labelsV2.length > 0) {
// If chore has labels, add duration to each label
chore.labelsV2.forEach(label => {
if (labelDurations[label.id]) {
labelDurations[label.id].duration += duration
} else {
labelDurations[label.id] = {
label: label.name,
duration: duration,
color: label.color || TASK_COLOR.ANYTIME,
id: label.id,
}
}
})
} else {
// If chore has no labels or chore not found, add to unlabeled
unlabeledDuration += duration
}
})
// Convert seconds to hours for better readability
const result = Object.values(labelDurations)
.map(item => ({
label: item.label,
value: Math.round((item.duration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal
color: item.color,
id: item.id,
}))
.filter(item => item.value > 0)
.sort((a, b) => b.value - a.value) // Sort by duration descending
// Add unlabeled tasks duration if there is any
if (unlabeledDuration > 0) {
result.push({
label: 'No Labels',
value: Math.round((unlabeledDuration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
})
}
return result
}
const generateTasksTimeChartData = history => {
if (!history || history.length === 0) {
return []
}
const taskDurations = {}
const colorValues = Object.values(COLORS)
// Iterate through ChoreHistory to get actual time spent per task
history.forEach(historyItem => {
const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory
const taskName = historyItem.choreName || 'Unknown Task'
if (taskDurations[taskName]) {
taskDurations[taskName].duration += duration
taskDurations[taskName].count += 1
} else {
taskDurations[taskName] = {
taskName: taskName,
duration: duration,
count: 1,
}
}
})
// Convert seconds to hours and prepare chart data
const result = Object.values(taskDurations)
.map((item, index) => ({
label: item.taskName,
value: Math.round((item.duration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal
count: item.count,
color: colorValues[index % colorValues.length],
id: item.taskName,
}))
.filter(item => item.value > 0)
.sort((a, b) => b.value - a.value) // Sort by time spent descending
.slice(0, 10) // Show top 10 tasks only
return result
}
const generateChoreDuePieChartData = chores => {
if (!chores || chores.length === 0) {
return []
}
const groups = ChoresGrouper('due_date', chores, null)
return groups
.map(group => {
return {
label: group.name,
value: group.content.length,
color: group.color,
id: group.name,
}
})
.filter(item => item.value > 0)
}
const generateHistoryPieChartData = history => {
if (!history || history.length === 0) {
return []
}
const totalCompleted =
history.filter(item => item.dueDate > item.performedAt).length || 0
const totalLate =
history.filter(item => item.dueDate < item.performedAt).length || 0
const totalNoDueDate = history.filter(item => !item.dueDate).length || 0
const result = []
if (totalCompleted > 0) {
result.push({
label: `On time`,
value: totalCompleted,
color: TASK_COLOR.COMPLETED,
id: 1,
})
}
if (totalLate > 0) {
result.push({
label: `Late`,
value: totalLate,
color: TASK_COLOR.LATE,
id: 2,
})
}
if (totalNoDueDate > 0) {
result.push({
label: `Completed`,
value: totalNoDueDate,
color: TASK_COLOR.ANYTIME,
id: 3,
})
}
return result
}
if (isChoresHistoryLoading || isChoresLoading) {
return <LoadingComponent />
}
const chartData = {
history: {
data: historyPieChartData || [],
title: 'Status',
description: 'Completed tasks status',
},
due: {
data: choreDuePieChartData || [],
title: 'Due Date',
description: 'Current tasks due date',
},
// assigned: {
// data: choresAssignedChartData,
// title: 'Assigned to me',
// description: 'Tasks assigned to you vs others',
// },
priority: {
data: choresPriorityChartData || [],
title: 'Priority',
description: 'Tasks by priority',
},
labels: {
data: choresLabelsChartData || [],
title: 'Labels',
description: 'Tasks by labels',
},
labelsDuration: {
data: choresLabelsDurationChartData || [],
title: 'Labels (time)',
description: 'Time spent by labels (hours)',
},
tasksTime: {
data: tasksTimeChartData || [],
title: 'Tasks (time)',
description: 'Time spent by individual tasks (hours)',
},
assigneeBreakdown: {
data: choresAssigneeBreakdownChartData || [],
title: 'by Assignee',
description: 'Tasks grouped by assignee',
},
}
if (!userProfile) {
return <LoadingComponent />
}
if (!choresData.res?.length > 0 || !choresHistory?.length > 0) {
return (
<Container
maxWidth='md'
sx={{
textAlign: 'center',
display: 'flex',
// make sure the content is centered vertically:
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
height: '50vh',
}}
>
<EventBusy
sx={{
fontSize: '6rem',
mb: 1,
}}
/>
<Typography level='h3' gutterBottom>
No activities
</Typography>
<Typography level='body1'>
You have no activities for the selected period.
</Typography>
<Button variant='soft' sx={{ mt: 2 }}>
<Link to='/my/chores'>Go back to chores</Link>
</Button>
</Container>
)
}
// Calculate activities analytics
return (
<Container
maxWidth='lg'
sx={{
display: 'flex',
flexDirection: 'column',
px: { xs: 2, sm: 3 },
}}
>
{/* Main Content Area - Mobile: Stack vertically, Desktop: Side by side */}
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', lg: 'row' },
gap: 3,
alignItems: 'flex-start',
}}
>
{/* Left Side - Timeline with Filters (Mobile: Full width, Desktop: Flexible) */}
<Box sx={{ flex: 1, minWidth: 0, width: '100%' }}>
{/* Improved Filter Bar - Now above timeline */}
<Card
variant='outlined'
sx={{
width: '100%',
p: 2,
mb: 3,
borderRadius: 12,
background:
'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
}}
>
<Stack spacing={2}>
<Typography level='title-sm' sx={{ color: 'text.secondary' }}>
Filter Activities
</Typography>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
alignItems={{ xs: 'stretch', sm: 'center' }}
>
{/* User Filter */}
<Box sx={{ flex: 1, minWidth: 200 }}>
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
Show activities for:
</Typography>
<Select
sx={{
width: '100%',
}}
variant='outlined'
value={selectedUser}
onChange={(e, selected) => {
setSelectedUser(selected)
setSelectedHistory(
enrichedHistory.filter(h => USER_FILTER(h, selected)),
)
}}
renderValue={() => {
if (
selectedUser === undefined ||
selectedUser === 'all'
) {
return (
<Typography
startDecorator={
<Avatar color='primary' size='sm'>
<Group />
</Avatar>
}
>
All Users
</Typography>
)
}
return (
<Typography
startDecorator={
<Avatar
color='primary'
size='sm'
src={resolvePhotoURL(
circleUsers.find(
user => user.userId === selectedUser,
)?.image,
)}
>
{circleUsers
.find(user => user.userId === selectedUser)
?.displayName?.charAt(0)}
</Avatar>
}
>
{
circleUsers.find(
user => user.userId === selectedUser,
)?.displayName
}
</Typography>
)
}}
>
<Option value='all'>
<Typography
startDecorator={
<Avatar color='primary' size='sm'>
<Group />
</Avatar>
}
>
All Users
</Typography>
</Option>
{circleUsers.map(user => (
<Option key={user.userId} value={user.userId}>
<Avatar
color='primary'
size='sm'
src={resolvePhotoURL(user.image)}
>
{user.displayName?.charAt(0)}
</Avatar>
<Typography>{user.displayName}</Typography>
<Chip
color='success'
size='sm'
variant='soft'
startDecorator={<Toll />}
>
{user.points - user.pointsRedeemed}
</Chip>
</Option>
))}
</Select>
</Box>
{/* Time Period Filter */}
<Box sx={{ flex: 1, minWidth: 200 }}>
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
Time period:
</Typography>
<Tabs
onChange={(e, tabValue) => {
setTabValue(tabValue)
refetchHistory(tabValue)
}}
value={tabValue}
sx={{
borderRadius: 8,
backgroundColor: 'background.surface',
border: '1px solid',
borderColor: 'divider',
}}
>
<TabList
disableUnderline
sx={{
borderRadius: 8,
backgroundColor: 'transparent',
p: 0.5,
gap: 0.5,
}}
>
{[
{ label: '7 Days', value: 7 },
{ label: '30 Days', value: 30 },
{ label: '90 Days', value: 90 },
{ label: 'All Time', value: 365 },
].map((tab, index) => (
<Tab
key={index}
sx={{
borderRadius: 6,
minWidth: 'auto',
px: 2,
py: 1,
fontSize: 'sm',
fontWeight: 500,
color: 'text.secondary',
'&.Mui-selected': {
color: 'primary.plainColor',
backgroundColor: 'primary.softBg',
fontWeight: 600,
},
'&:hover': {
backgroundColor: 'neutral.softHoverBg',
},
}}
disableIndicator
value={tab.value}
>
{tab.label}
</Tab>
))}
</TabList>
</Tabs>
</Box>
</Stack>
</Stack>
</Card>
{/* Current Filter Summary */}
<Box sx={{ mb: 3, textAlign: 'center' }}>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
Showing activities 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>{' '}
over the{' '}
<Typography
component='span'
sx={{ fontWeight: 600, color: 'primary.500' }}
>
{tabValue === 365 ? 'All Time' : `Last ${tabValue} Days`}
</Typography>
</Typography>
</Box>
<ChoreHistoryTimeline history={selectedHistory} />
</Box>
{/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */}
<Box
sx={{
width: { xs: '100%', lg: '350px' },
position: { xs: 'static', lg: 'sticky' },
top: { lg: '60px' },
alignSelf: { lg: 'flex-start' },
maxHeight: { lg: 'calc(100vh - 40px)' },
overflowY: { lg: 'auto' },
order: { xs: -1, lg: 1 }, // Show charts first on mobile, last on desktop
}}
>
{/* Charts Container */}
<Card
variant='plain'
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: 'sm',
borderRadius: 20,
width: { xs: '100%', lg: '315px' },
mr: { xs: 0, lg: 10 },
mb: 1,
}}
>
<Stack spacing={3}>
{/* Main Chart */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
textAlign: 'center',
minHeight: {
lg:
chartData[selectedChart].data.length <= 3
? '350px'
: '450px',
}, // Dynamic height based on legend needs
maxHeight: {
lg:
chartData[selectedChart].data.length <= 3
? '400px'
: '500px',
},
}}
>
<Typography level='h4' textAlign='center' sx={{ mb: 1 }}>
{chartData[selectedChart].title}
</Typography>
<Typography level='body-xs' textAlign='center' sx={{ mb: 2 }}>
{chartData[selectedChart].description}
</Typography>
<Box
sx={{
flex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
width: '100%',
}}
>
{renderPieChart(
chartData[selectedChart].data,
300, // Increased size for better chart container
true,
selectedChart,
)}
</Box>
</Box>
<Divider />
{/* Chart Selection Grid */}
<Box>
<Grid container spacing={1}>
{Object.entries(chartData)
.filter(([key]) => key !== selectedChart)
.map(([key, { data, title }]) => (
<Grid
item
key={key}
xs={4}
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Card
onClick={() => setSelectedChart(key)}
variant='plain'
sx={{
cursor: 'pointer',
p: 1,
transition: 'all 0.2s ease-in-out',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: 80,
maxWidth: 90,
'&:hover': {
transform: 'scale(1.02)',
boxShadow: 'sm',
},
}}
>
<Typography
textAlign='center'
level='body-xs'
sx={{
mb: 0.5,
fontSize: '0.65rem',
lineHeight: 1.2,
}}
>
{title}
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{renderPieChart(data, 70, false)}
</Box>
</Card>
</Grid>
))}
</Grid>
</Box>
</Stack>
</Card>
</Box>
</Box>
</Container>
)
}
export default UserActivites