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
This commit is contained in:
@@ -3,7 +3,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import CircleIcon from '@mui/icons-material/Circle'
|
||||
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
|
||||
|
||||
import { EventBusy, Group, Toll } from '@mui/icons-material'
|
||||
import { EventBusy, Group, Timeline, Toll } from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -94,9 +94,12 @@ const ChoreHistoryTimeline = ({ history }) => {
|
||||
|
||||
return (
|
||||
<Container sx={{ p: 2 }}>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Activities Timeline
|
||||
</Typography>
|
||||
<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 }}>
|
||||
@@ -785,6 +788,7 @@ const UserActivites = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate activities analytics
|
||||
return (
|
||||
<Container
|
||||
maxWidth='lg'
|
||||
@@ -794,16 +798,6 @@ const UserActivites = () => {
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
mb={3}
|
||||
level='h4'
|
||||
sx={{
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
Activities Overview
|
||||
</Typography>
|
||||
|
||||
{/* Main Content Area - Mobile: Stack vertically, Desktop: Side by side */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -1040,24 +1034,17 @@ const UserActivites = () => {
|
||||
<Card
|
||||
variant='plain'
|
||||
sx={{
|
||||
// maxHeight: { lg: '90vh' },
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mr: 10,
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
width: '315px',
|
||||
width: { xs: '100%', lg: '315px' },
|
||||
mr: { xs: 0, lg: 10 },
|
||||
mb: 1,
|
||||
}}
|
||||
// variant='outlined'
|
||||
// sx={{
|
||||
// p: 2,
|
||||
// borderRadius: 12,
|
||||
// backdropFilter: 'blur(10px)',
|
||||
// }}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
{/* Main Chart */}
|
||||
|
||||
@@ -7,14 +7,29 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
|
||||
import { CreditCard, Toll } from '@mui/icons-material'
|
||||
import {
|
||||
AccountBalanceWallet,
|
||||
Analytics,
|
||||
CreditCard,
|
||||
EmojiEvents,
|
||||
MilitaryTech,
|
||||
Redeem,
|
||||
Star,
|
||||
SwapHoriz,
|
||||
Timeline,
|
||||
Toll,
|
||||
TrendingUp,
|
||||
WorkspacePremium,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Container,
|
||||
Grid,
|
||||
Option,
|
||||
Select,
|
||||
Stack,
|
||||
@@ -34,6 +49,7 @@ import RedeemPointsModal from '../Modals/RedeemPointsModal'
|
||||
const UserPoints = () => {
|
||||
const [tabValue, setTabValue] = useState(7)
|
||||
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
|
||||
const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
|
||||
|
||||
const {
|
||||
data: circleMembersData,
|
||||
@@ -206,24 +222,371 @@ const UserPoints = () => {
|
||||
return <LoadingComponent />
|
||||
}
|
||||
|
||||
// Calculate leaderboard data for the current time period
|
||||
const calculateLeaderboard = () => {
|
||||
if (!choresHistoryData || !circleUsers.length) return []
|
||||
|
||||
// Calculate points for each user in the current time period
|
||||
const userPeriodStats = {}
|
||||
|
||||
// Initialize stats for all users
|
||||
circleUsers.forEach(user => {
|
||||
userPeriodStats[user.userId] = {
|
||||
userId: user.userId,
|
||||
displayName: user.displayName,
|
||||
image: user.image,
|
||||
totalPoints: user.points || 0,
|
||||
availablePoints: (user.points || 0) - (user.pointsRedeemed || 0),
|
||||
periodPoints: 0,
|
||||
periodTasks: 0,
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate period-specific stats from history
|
||||
choresHistoryData.forEach(historyEntry => {
|
||||
const userId = historyEntry.completedBy
|
||||
if (userPeriodStats[userId]) {
|
||||
userPeriodStats[userId].periodPoints += historyEntry.points || 0
|
||||
userPeriodStats[userId].periodTasks += 1
|
||||
}
|
||||
})
|
||||
|
||||
// Convert to array and sort by selected mode
|
||||
const sortField =
|
||||
leaderboardMode === 'points' ? 'periodPoints' : 'periodTasks'
|
||||
return Object.values(userPeriodStats)
|
||||
.sort((a, b) => b[sortField] - a[sortField])
|
||||
.map((user, index) => ({
|
||||
...user,
|
||||
rank: index + 1,
|
||||
avgPointsPerTask:
|
||||
user.periodTasks > 0
|
||||
? (user.periodPoints / user.periodTasks).toFixed(1)
|
||||
: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
const leaderboardData = calculateLeaderboard()
|
||||
|
||||
// Get trophy icons for top 3
|
||||
const getTrophyIcon = rank => {
|
||||
switch (rank) {
|
||||
case 1:
|
||||
return <EmojiEvents sx={{ color: '#FFD700', fontSize: '1.2rem' }} /> // Gold
|
||||
case 2:
|
||||
return (
|
||||
<WorkspacePremium sx={{ color: '#C0C0C0', fontSize: '1.2rem' }} />
|
||||
) // Silver
|
||||
case 3:
|
||||
return <MilitaryTech sx={{ color: '#CD7F32', fontSize: '1.2rem' }} /> // Bronze
|
||||
default:
|
||||
return <Star sx={{ color: 'text.secondary', fontSize: '1rem' }} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
maxWidth='xl'
|
||||
maxWidth='md'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
mb={3}
|
||||
level='h4'
|
||||
sx={{
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
Points Overview
|
||||
</Typography>
|
||||
{/* Enhanced Leaderboard Header Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Stack spacing={2}>
|
||||
{/* Title Row */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<EmojiEvents sx={{ fontSize: '2rem', color: '#FFD700' }} />
|
||||
<Stack sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
level='h3'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
{leaderboardMode === 'points' ? 'Points' : 'Tasks'} Leaderboard
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
Rankings based on{' '}
|
||||
{leaderboardMode === 'points'
|
||||
? 'points earned'
|
||||
: 'tasks completed'}{' '}
|
||||
during the selected time period
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Filters Row - Responsive */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 2,
|
||||
alignItems: { xs: 'stretch', sm: 'center' },
|
||||
justifyContent: { xs: 'flex-start', sm: 'space-between' },
|
||||
}}
|
||||
>
|
||||
{/* Time Period Filter */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
onChange={(e, tabValue) => {
|
||||
setTabValue(tabValue)
|
||||
handleChoresHistoryLimitChange(tabValue)
|
||||
}}
|
||||
value={tabValue}
|
||||
size='sm'
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
backgroundColor: 'background.surface',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<TabList
|
||||
disableUnderline
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
backgroundColor: 'transparent',
|
||||
p: 0.3,
|
||||
gap: 0.3,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: '7D', value: 7 },
|
||||
{ label: '6M', value: 6 * 30 },
|
||||
{ label: 'All', value: 24 * 30 },
|
||||
].map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
sx={{
|
||||
borderRadius: 4,
|
||||
minWidth: 'auto',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
fontSize: 'xs',
|
||||
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>
|
||||
|
||||
{/* Toggle between points and tasks */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: { xs: 'center', sm: 'flex-end' },
|
||||
gap: 1,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
variant={leaderboardMode === 'points' ? 'solid' : 'outlined'}
|
||||
color='primary'
|
||||
size='sm'
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => setLeaderboardMode('points')}
|
||||
>
|
||||
Points
|
||||
</Chip>
|
||||
<SwapHoriz
|
||||
sx={{ fontSize: '0.875rem', color: 'text.tertiary' }}
|
||||
/>
|
||||
<Chip
|
||||
variant={leaderboardMode === 'tasks' ? 'solid' : 'outlined'}
|
||||
color='primary'
|
||||
size='sm'
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => setLeaderboardMode('tasks')}
|
||||
>
|
||||
Tasks
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Leaderboard Cards */}
|
||||
<Card
|
||||
variant='outlined'
|
||||
sx={{ borderRadius: 'lg', overflow: 'hidden' }}
|
||||
>
|
||||
<Stack spacing={0}>
|
||||
{leaderboardData.map((user, index) => (
|
||||
<Box key={user.userId}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
p: 2.5,
|
||||
backgroundColor:
|
||||
user.userId === userProfile.id
|
||||
? 'primary.softBg'
|
||||
: 'transparent',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
user.userId === userProfile.id
|
||||
? 'primary.softHoverBg'
|
||||
: 'neutral.softHoverBg',
|
||||
},
|
||||
cursor:
|
||||
user.userId === selectedUser ? 'default' : 'pointer',
|
||||
transition: 'background-color 0.2s ease',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (user.userId !== selectedUser) {
|
||||
setSelectedUser(user.userId)
|
||||
setSelectedHistory(
|
||||
generateWeeklySummary(choresHistoryData, user.userId),
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Rank Badge */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: 40,
|
||||
mr: 2,
|
||||
}}
|
||||
>
|
||||
{getTrophyIcon(user.rank)}
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
fontWeight: user.rank <= 3 ? 'bold' : 'normal',
|
||||
color:
|
||||
user.rank <= 3 ? 'text.primary' : 'text.secondary',
|
||||
}}
|
||||
>
|
||||
#{user.rank}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* User Avatar and Info */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||
<Avatar
|
||||
src={user.image ? resolvePhotoURL(user.image) : undefined}
|
||||
sx={{ width: 40, height: 40, mr: 2 }}
|
||||
>
|
||||
{user.displayName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Stack sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
fontWeight:
|
||||
user.userId === userProfile.id ? 'bold' : 'normal',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
{user.displayName}
|
||||
{user.userId === userProfile.id && (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='primary'
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
You
|
||||
</Chip>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
{user.periodTasks} tasks • {user.avgPointsPerTask} avg
|
||||
per task
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Metric Display */}
|
||||
<Stack alignItems='flex-end' spacing={0.5}>
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
||||
>
|
||||
<Toll sx={{ fontSize: '1rem', color: 'success.500' }} />
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
color: 'success.600',
|
||||
}}
|
||||
>
|
||||
{leaderboardMode === 'points'
|
||||
? user.periodPoints
|
||||
: user.periodTasks}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
||||
{leaderboardMode === 'points'
|
||||
? `${user.availablePoints} available`
|
||||
: `${user.periodPoints} points`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{/* Selection Indicator */}
|
||||
{user.userId === selectedUser && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 4,
|
||||
backgroundColor: 'primary.500',
|
||||
borderRadius: '0 4px 4px 0',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{index < leaderboardData.length - 1 && (
|
||||
<Box
|
||||
sx={{
|
||||
height: 1,
|
||||
backgroundColor: 'divider',
|
||||
mx: 2.5,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Filters Section Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Analytics sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
|
||||
<Typography level='h4' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
|
||||
Filter & Analysis
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Improved Filter Bar */}
|
||||
<Card
|
||||
@@ -397,6 +760,107 @@ const UserPoints = () => {
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Points Status Cards */}
|
||||
<Grid container spacing={1} sx={{ mb: 3 }}>
|
||||
{(() => {
|
||||
const selectedUserData = circleUsers.find(
|
||||
user => user.userId === selectedUser,
|
||||
)
|
||||
const totalPoints = selectedUserData?.points || 0
|
||||
const redeemedPoints = selectedUserData?.pointsRedeemed || 0
|
||||
const availablePoints = totalPoints - redeemedPoints
|
||||
|
||||
const periodStats = leaderboardData.find(
|
||||
user => user.userId === selectedUser,
|
||||
)
|
||||
const periodPoints = selectedHistory.reduce(
|
||||
(sum, item) => sum + (item.points || 0),
|
||||
0,
|
||||
)
|
||||
|
||||
const pointsCards = [
|
||||
{
|
||||
icon: <AccountBalanceWallet />,
|
||||
title: 'Total',
|
||||
text: `${totalPoints} points`,
|
||||
subtext: 'All time earned',
|
||||
},
|
||||
{
|
||||
icon: <Toll />,
|
||||
title: 'Available',
|
||||
text: `${availablePoints} points`,
|
||||
subtext: 'Ready to redeem',
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp />,
|
||||
title: 'Period Points',
|
||||
text: `${periodPoints} points`,
|
||||
subtext: `${tabValue === 24 * 30 ? 'All time' : tabValue === 6 * 30 ? 'Last 6 months' : `Last ${tabValue} days`}`,
|
||||
},
|
||||
{
|
||||
icon: <Redeem />,
|
||||
title: 'Redeemed',
|
||||
text: `${redeemedPoints} points`,
|
||||
subtext: 'Previously used',
|
||||
},
|
||||
]
|
||||
|
||||
return pointsCards.map((card, index) => (
|
||||
<Grid item xs={6} sm={6} key={index}>
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
borderRadius: 'md',
|
||||
boxShadow: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
minHeight: 90,
|
||||
height: '100%',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{card.icon}
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
ml: 1,
|
||||
fontWeight: '500',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
{card.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'text.secondary', lineHeight: 1.5 }}
|
||||
>
|
||||
{card.text}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'text.secondary', lineHeight: 1.5 }}
|
||||
>
|
||||
{card.subtext}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))
|
||||
})()}
|
||||
</Grid>
|
||||
|
||||
{/* Current Filter Summary */}
|
||||
<Box sx={{ mb: 3, textAlign: 'center' }}>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
@@ -430,107 +894,15 @@ const UserPoints = () => {
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
{/* Points Cards */}
|
||||
<Box
|
||||
sx={{
|
||||
// resposive width based on parent available space:
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-evenly',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
title: 'Total',
|
||||
value: circleMembersData.res.find(
|
||||
user => user.userId === selectedUser,
|
||||
)?.points,
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Available',
|
||||
value: (function () {
|
||||
const user = circleMembersData.res.find(
|
||||
user => user.userId === selectedUser,
|
||||
)
|
||||
if (!user) return 0
|
||||
return user.points - user.pointsRedeemed
|
||||
})(),
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Redeemed',
|
||||
value: circleMembersData.res.find(
|
||||
user => user.userId === selectedUser,
|
||||
)?.pointsRedeemed,
|
||||
color: 'warning',
|
||||
},
|
||||
].map(card => (
|
||||
<Card
|
||||
key={card.title}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 1,
|
||||
minWidth: 80,
|
||||
width: '100%',
|
||||
}}
|
||||
variant='soft'
|
||||
>
|
||||
<Typography level='body-xs' textAlign='center' mb={-1}>
|
||||
{card.title}
|
||||
</Typography>
|
||||
<Typography level='title-md' textAlign='center'>
|
||||
{card.value}
|
||||
</Typography>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Points History Section */}
|
||||
<Typography level='h4' sx={{ mt: 2, mb: 2 }}>
|
||||
Points History
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
// resposive width based on parent available space:
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'left',
|
||||
gap: 1,
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
title: 'Points',
|
||||
value: selectedHistory.reduce((acc, cur) => acc + cur.points, 0),
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Tasks',
|
||||
value: selectedHistory.reduce((acc, cur) => acc + cur.tasks, 0),
|
||||
color: 'primary',
|
||||
},
|
||||
].map(card => (
|
||||
<Card
|
||||
key={card.title}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 1,
|
||||
width: 250,
|
||||
}}
|
||||
variant='soft'
|
||||
>
|
||||
<Typography level='body-xs' textAlign='center' mb={-1}>
|
||||
{card.title}
|
||||
</Typography>
|
||||
<Typography level='title-md' textAlign='center'>
|
||||
{card.value}
|
||||
</Typography>
|
||||
</Card>
|
||||
))}
|
||||
{/* Chart Section Header */}
|
||||
<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' }}
|
||||
>
|
||||
Points Trend
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Bar Chart for points overtime */}
|
||||
|
||||
Reference in New Issue
Block a user