Files
donetick/src/views/User/UserPoints.jsx
everysingletear 72aa57f018 i18n: extract things, history, projects, labels, filters, timer and points
Part of #145.

Fourteen files across seven feature areas that had no namespace yet: the
things create/edit modals and their history, the chore history detail and
edit modals, the activity feed, the points view and its redemption modal,
the project view with its selector and icon picker, the label view, the
advanced filter builder and the timer edit modal.

Seven new namespaces registered in `src/i18n/config.js` in one change
rather than one per PR, so the `ns:` array is touched once and my other
extraction PRs cannot conflict with this one. Namespaces stay
feature-scoped as described in #145; if you'd rather fold any of these
into `common` or `chores`, say which and I'll rework it.

Dictionaries: `history` 57 keys, `points` 50, `timer` 25, `projects` 16,
`things` 14, `filters` 13, `labels` 5.

English only — no translations, no behaviour change. Every t() value is
checked against this branch's base: the string must appear
character-for-character in the code it replaces (226 call sites).

Three values in `UserPoints` are matched loosely and worth naming. The
base builds the leaderboard heading and subtitle around a ternary —
`{mode === 'points' ? 'Points' : 'Tasks'} Leaderboard` and `Rankings based
on {…} during the selected time period` — so neither full sentence exists
contiguously in the source. Each key holds exactly what one branch
renders. The sentences are kept whole rather than split around the
ternary, since a sentence assembled from fragments cannot be reordered by
a translator.
2026-08-13 10:21:57 +08:00

974 lines
32 KiB
JavaScript

import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
XAxis,
YAxis,
} from 'recharts'
import {
AccountBalanceWallet,
Analytics,
AssignmentTurnedIn,
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,
Tab,
TabList,
Tabs,
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
import moment from 'moment'
import { useTranslation } from 'react-i18next'
import LoadingComponent from '../components/Loading.jsx'
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { RedeemPoints } from '../../utils/Fetcher.jsx'
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
import RedeemPointsModal from '../Modals/RedeemPointsModal'
const UserPoints = () => {
const { t } = useTranslation('points')
const [tabValue, setTabValue] = useState(7)
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
const {
data: circleMembersData,
isLoading: isCircleMembersLoading,
handleRefetch: handleCircleMembersRefetch,
} = useCircleMembers()
const {
data: choresHistoryData,
isLoading: isChoresHistoryLoading,
handleLimitChange: handleChoresHistoryLimitChange,
} = useChoresHistory(7, true)
const { data: userProfile } = useUserProfile()
const [selectedUser, setSelectedUser] = useState(userProfile?.id)
const [circleUsers, setCircleUsers] = useState([])
const [selectedHistory, setSelectedHistory] = useState([])
useEffect(() => {
if (circleMembersData && choresHistoryData && userProfile) {
setCircleUsers(circleMembersData.res)
setSelectedHistory(
generateWeeklySummary(choresHistoryData, userProfile?.id),
)
}
}, [circleMembersData, choresHistoryData, userProfile])
useEffect(() => {
if (choresHistoryData) {
var history
if (tabValue === 7) {
history = generateWeeklySummary(choresHistoryData, selectedUser)
} else if (tabValue === 30) {
history = generateMonthSummary(choresHistoryData, selectedUser)
} else if (tabValue === 6 * 30) {
history = generateMonthlySummary(choresHistoryData, selectedUser)
} else {
history = generateYearlySummary(choresHistoryData, selectedUser)
}
setSelectedHistory(history)
}
}, [selectedUser, choresHistoryData, tabValue])
useEffect(() => {
setSelectedUser(userProfile?.id)
}, [userProfile])
const generateWeeklySummary = (history, userId) => {
const daysAggregated = []
for (let i = 6; i > -1; i--) {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
label: moment(currentDate).format('ddd'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const dayName = moment(chore.performedAt).format('ddd')
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
return dayData.label === dayName && chore.completedBy === userId
return dayData.label === dayName
})
if (dayIndex !== -1) {
if (chore.points) daysAggregated[dayIndex].points += chore.points
daysAggregated[dayIndex].tasks += 1
}
})
return daysAggregated
}
const generateMonthSummary = (history, userId) => {
const daysAggregated = []
for (let i = 29; i > -1; i--) {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
label: moment(currentDate).format('D'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const dayName = moment(chore.performedAt).format('D')
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
return dayData.label === dayName && chore.completedBy === userId
return dayData.label === dayName
})
if (dayIndex !== -1) {
if (chore.points) daysAggregated[dayIndex].points += chore.points
daysAggregated[dayIndex].tasks += 1
}
})
return daysAggregated
}
const generateMonthlySummary = (history, userId) => {
const monthlyAggregated = []
for (let i = 5; i > -1; i--) {
const currentMonth = new Date()
currentMonth.setMonth(currentMonth.getMonth() - i)
monthlyAggregated.push({
label: moment(currentMonth).format('MMM'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const monthName = moment(chore.performedAt).format('MMM')
const monthIndex = monthlyAggregated.findIndex(monthData => {
if (userId)
return monthData.label === monthName && chore.completedBy === userId
return monthData.label === monthName
})
if (monthIndex !== -1) {
if (chore.points) monthlyAggregated[monthIndex].points += chore.points
monthlyAggregated[monthIndex].tasks += 1
}
})
return monthlyAggregated
}
const generateYearlySummary = (history, userId) => {
const yearlyAggregated = []
for (let i = 11; i > -1; i--) {
const currentYear = new Date()
currentYear.setFullYear(currentYear.getFullYear() - i)
yearlyAggregated.push({
label: moment(currentYear).format('YYYY'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const yearName = moment(chore.performedAt).format('YYYY')
const yearIndex = yearlyAggregated.findIndex(yearData => {
if (userId)
return yearData.label === yearName && chore.completedBy === userId
return yearData.label === yearName
})
if (yearIndex !== -1) {
if (chore.points) yearlyAggregated[yearIndex].points += chore.points
yearlyAggregated[yearIndex].tasks += 1
}
})
return yearlyAggregated
}
if (isChoresHistoryLoading || isCircleMembersLoading || !userProfile) {
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='md'
sx={{
display: 'flex',
flexDirection: 'column',
px: { xs: 2, sm: 3 },
}}
>
{/* 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'
? t('leaderboard.titlePoints')
: t('leaderboard.titleTasks')}
</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{leaderboardMode === 'points'
? t('leaderboard.subtitlePoints')
: t('leaderboard.subtitleTasks')}
</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: t('tabs.short7d'), value: 7 },
{ label: t('tabs.short6m'), value: 6 * 30 },
{ label: t('tabs.shortAll'), 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')}
>
{t('leaderboard.modePoints')}
</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')}
>
{t('leaderboard.modeTasks')}
</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 }}
>
{t('leaderboard.you')}
</Chip>
)}
</Typography>
<Typography
level='body-xs'
sx={{ color: 'text.secondary' }}
>
{t('leaderboard.tasksAndAvg', {
tasks: user.periodTasks,
avg: user.avgPointsPerTask,
})}
</Typography>
</Stack>
</Box>
{/* Metric Display */}
<Stack alignItems='flex-end' spacing={0.5}>
<Box
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
>
{leaderboardMode === 'points' ? (
<Toll sx={{ fontSize: '1rem', color: 'success.500' }} />
) : (
<AssignmentTurnedIn
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'
? t('leaderboard.available', {
count: user.availablePoints,
})
: t('leaderboard.points', {
count: user.periodPoints,
})}
</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' }}>
{t('filter.analysisTitle')}
</Typography>
</Box>
{/* Improved Filter Bar */}
<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' }}>
{t('filter.title')}
</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 }}>
{t('filter.showFor')}
</Typography>
<Select
sx={{
width: '100%',
}}
variant='outlined'
value={selectedUser}
onChange={(e, selected) => {
setSelectedUser(selected)
setSelectedHistory(
generateWeeklySummary(choresHistoryData, selected),
)
}}
renderValue={() => {
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>
)
}}
>
{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 }}>
{t('filter.timePeriod')}
</Typography>
<Tabs
onChange={(e, tabValue) => {
setTabValue(tabValue)
handleChoresHistoryLimitChange(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: t('tabs.days7'), value: 7 },
{ label: t('tabs.months6'), value: 6 * 30 },
{ label: t('tabs.allTime'), value: 24 * 30 },
].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>
{/* Redeem Points Button */}
{circleUsers.find(user => user.userId === userProfile.id)?.role ===
'admin' && (
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<Button
variant='soft'
size='md'
startDecorator={<CreditCard />}
onClick={() => {
setIsRedeemModalOpen(true)
}}
sx={{ mt: 'auto' }}
>
{t('redeem')}
</Button>
</Box>
)}
</Stack>
</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: <Toll />,
title: t('cards.available.title'),
text: t('leaderboard.points', { count: availablePoints }),
subtext: t('cards.available.subtext'),
},
{
icon: <Redeem />,
title: t('cards.redeemed.title'),
text: t('leaderboard.points', { count: redeemedPoints }),
subtext: t('cards.redeemed.subtext'),
},
{
icon: <AccountBalanceWallet />,
title: t('cards.total.title'),
text: t('leaderboard.points', { count: totalPoints }),
subtext: t('cards.total.subtext'),
},
{
icon: <TrendingUp />,
title: t('cards.period.title'),
text: t('leaderboard.points', { count: periodPoints }),
subtext:
tabValue === 24 * 30
? t('cards.period.allTime')
: tabValue === 6 * 30
? t('cards.period.last6Months')
: t('cards.period.lastDays', { count: tabValue }),
},
]
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' }}>
{t('summary.showingFor')}{' '}
<Typography
component='span'
sx={{ fontWeight: 600, color: 'primary.500' }}
>
{circleUsers.find(user => user.userId === selectedUser)
?.displayName || t('summary.unknownUser')}
</Typography>{' '}
{t('summary.overThe')}{' '}
<Typography
component='span'
sx={{ fontWeight: 600, color: 'primary.500' }}
>
{tabValue === 24 * 30
? t('summary.allTime')
: tabValue === 6 * 30
? t('summary.last6Months')
: t('summary.lastDays', { count: tabValue })}
</Typography>
</Typography>
</Box>
<Box
sx={{
mb: 4,
display: 'flex',
flexDirection: 'column',
gap: 3,
}}
>
{/* 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' }}
>
{t('trend.title')}
</Typography>
</Box>
{/* Bar Chart for points overtime */}
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 1 }}>
<ResponsiveContainer height={300}>
<BarChart
data={selectedHistory}
margin={{ top: 5, left: -20, bottom: 5 }}
>
<CartesianGrid strokeDasharray={'3 3'} />
<XAxis dataKey='label' axisLine={false} tickLine={false} />
<YAxis axisLine={false} tickLine={false} />
<Bar
fill='#4183F2'
dataKey='points'
barSize={30}
radius={[5, 5, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Box>
</Box>
<RedeemPointsModal
config={(() => {
const user = circleUsers.find(u => u.userId === selectedUser)
const availablePoints = user
? (user.points || 0) - (user.pointsRedeemed || 0)
: 0
return {
onClose: () => {
setIsRedeemModalOpen(false)
},
isOpen: isRedeemModalOpen,
available: availablePoints,
user: user,
onSave: ({ userId, points }) => {
RedeemPoints(userId, points, userProfile.circleID)
.then(() => {
setIsRedeemModalOpen(false)
handleCircleMembersRefetch()
})
.catch(err => {
console.log(err)
})
},
}
})()}
/>
</Container>
)
}
export default UserPoints