Add Points view, Don't allow completion outside the completion window, migrate more things to react query

This commit is contained in:
Mo Tarbin 2024-12-31 02:23:21 -05:00
commit c5e89c1fb6
14 changed files with 628 additions and 30 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "donetick", "name": "donetick",
"private": true, "private": true,
"version": "0.1.85", "version": "0.1.90",
"type": "module", "type": "module",
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx}": [ "*.{js,jsx,ts,tsx}": [

View file

@ -23,6 +23,7 @@ import TestView from '../views/TestView/Test'
import ThingsHistory from '../views/Things/ThingsHistory' import ThingsHistory from '../views/Things/ThingsHistory'
import ThingsView from '../views/Things/ThingsView' import ThingsView from '../views/Things/ThingsView'
import UserActivities from '../views/User/UserActivities' import UserActivities from '../views/User/UserActivities'
import UserPoints from '../views/User/UserPoints'
const getMainRoute = () => { const getMainRoute = () => {
if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') { if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') {
return <Landing /> return <Landing />
@ -71,6 +72,10 @@ const Router = createBrowserRouter([
path: '/activities', path: '/activities',
element: <UserActivities />, element: <UserActivities />,
}, },
{
path: '/points',
element: <UserPoints />,
},
{ {
path: '/login', path: '/login',
element: <LoginView />, element: <LoginView />,

View file

@ -19,11 +19,11 @@ export const useCreateChore = () => {
}) })
} }
export const useChoresHistory = initialLimit => { export const useChoresHistory = (initialLimit, includeMembers) => {
const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected
const { data, error, isLoading } = useQuery(['choresHistory', limit], () => const { data, error, isLoading } = useQuery(['choresHistory', limit], () =>
GetChoresHistory(limit), GetChoresHistory(limit, includeMembers),
) )
const handleLimitChange = newLimit => { const handleLimitChange = newLimit => {

View file

@ -1,6 +1,10 @@
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { GetAllUsers } from '../utils/Fetcher' import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
export const useAllUsers = () => { export const useAllUsers = () => {
return useQuery('allUsers', GetAllUsers) return useQuery('allUsers', GetAllUsers)
} }
export const useCircleMembers = () => {
return useQuery('allCircleMembers', GetAllCircleMembers)
}

View file

@ -1,3 +1,4 @@
import moment from 'moment'
import { TASK_COLOR } from './Colors.jsx' import { TASK_COLOR } from './Colors.jsx'
export const ChoresGrouper = (groupBy, chores) => { export const ChoresGrouper = (groupBy, chores) => {
@ -158,3 +159,12 @@ export const ChoresGrouper = (groupBy, chores) => {
} }
return groups return groups
} }
export const notInCompletionWindow = chore => {
return (
chore.completionWindow &&
chore.completionWindow > -1 &&
chore.nextDueDate &&
moment().add(chore.completionWindow, 'hours') < moment(chore.nextDueDate)
)
}

View file

@ -11,7 +11,8 @@ const createChore = userID => {
} }
const signUp = (username, password, displayName, email) => { const signUp = (username, password, displayName, email) => {
return fetch(`/auth/`, { const baseURL = apiManager.getApiURL()
return fetch(`${baseURL}/auth/`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -193,11 +194,12 @@ const UpdateChoreHistory = (choreId, id, choreHistory) => {
}) })
} }
const GetAllCircleMembers = () => { const GetAllCircleMembers = async () => {
return Fetch(`/circles/members`, { const resp = await Fetch(`/circles/members`, {
method: 'GET', method: 'GET',
headers: HEADERS(), headers: HEADERS(),
}) })
return resp.json()
} }
const GetUserProfile = () => { const GetUserProfile = () => {
@ -420,11 +422,16 @@ const RefreshToken = () => {
headers: HEADERS(), headers: HEADERS(),
}) })
} }
const GetChoresHistory = async limit => { const GetChoresHistory = async (limit, includeMembers) => {
var url = `/chores/history` var url = `/chores/history`
if (!limit) limit = 7
if (limit) { if (limit) {
url += `?limit=${limit}` url += `?limit=${limit}`
} }
if (includeMembers) {
url += `&members=true`
}
const resp = await Fetch(url, { const resp = await Fetch(url, {
method: 'GET', method: 'GET',
headers: HEADERS(), headers: HEADERS(),

View file

@ -75,6 +75,7 @@ const ChoreEdit = () => {
const [labels, setLabels] = useState([]) const [labels, setLabels] = useState([])
const [labelsV2, setLabelsV2] = useState([]) const [labelsV2, setLabelsV2] = useState([])
const [points, setPoints] = useState(-1) const [points, setPoints] = useState(-1)
const [completionWindow, setCompletionWindow] = useState(-1)
const [allUserThings, setAllUserThings] = useState([]) const [allUserThings, setAllUserThings] = useState([])
const [thingTrigger, setThingTrigger] = useState(null) const [thingTrigger, setThingTrigger] = useState(null)
const [isThingValid, setIsThingValid] = useState(false) const [isThingValid, setIsThingValid] = useState(false)
@ -200,6 +201,7 @@ const ChoreEdit = () => {
notificationMetadata: notificationMetadata, notificationMetadata: notificationMetadata,
thingTrigger: thingTrigger, thingTrigger: thingTrigger,
points: points < 0 ? null : points, points: points < 0 ? null : points,
completionWindow: completionWindow < 0 ? null : completionWindow,
} }
let SaveFunction = CreateChore let SaveFunction = CreateChore
if (choreId > 0) { if (choreId > 0) {
@ -216,9 +218,7 @@ const ChoreEdit = () => {
} }
useEffect(() => { useEffect(() => {
//fetch performers: //fetch performers:
GetAllCircleMembers() GetAllCircleMembers().then(data => {
.then(response => response.json())
.then(data => {
setPerformers(data.res) setPerformers(data.res)
}) })
GetThings().then(response => { GetThings().then(response => {
@ -254,7 +254,11 @@ const ChoreEdit = () => {
setPoints( setPoints(
data.res.points && data.res.points > -1 ? data.res.points : -1, data.res.points && data.res.points > -1 ? data.res.points : -1,
) )
// setLabels(data.res.labels ? data.res.labels.split(',') : []) setCompletionWindow(
data.res.completionWindow && data.res.completionWindow > -1
? data.res.completionWindow
: -1,
)
setLabelsV2(data.res.labelsV2) setLabelsV2(data.res.labelsV2)
setAssignStrategy( setAssignStrategy(
@ -570,6 +574,70 @@ const ChoreEdit = () => {
<FormHelperText>{errors.dueDate}</FormHelperText> <FormHelperText>{errors.dueDate}</FormHelperText>
</FormControl> </FormControl>
)} )}
<FormControl
orientation='horizontal'
sx={{ width: 400, justifyContent: 'space-between' }}
>
<div>
{/* <FormLabel>Completion window (hours)</FormLabel> */}
<Typography level='h5'>Completion window (hours)</Typography>
<FormHelperText sx={{ mt: 0 }}>
{"Set a time window that task can't be completed before"}
</FormHelperText>
</div>
<Switch
checked={completionWindow != -1}
onClick={event => {
event.preventDefault()
if (completionWindow != -1) {
setCompletionWindow(-1)
} else {
setCompletionWindow(1)
}
}}
color={completionWindow !== -1 ? 'success' : 'neutral'}
variant={completionWindow !== -1 ? 'solid' : 'outlined'}
// endDecorator={points !== -1 ? 'On' : 'Off'}
slotProps={{
endDecorator: {
sx: {
minWidth: 24,
},
},
}}
/>
</FormControl>
{completionWindow != -1 && (
<Card variant='outlined'>
<Box
sx={{
mt: 0,
ml: 4,
}}
>
<Typography level='body-sm'>Hours:</Typography>
<Input
type='number'
value={completionWindow}
sx={{ maxWidth: 100 }}
// add min points is 0 and max is 1000
slotProps={{
input: {
min: 0,
max: 24 * 7,
},
}}
placeholder='Hours'
onChange={e => {
setCompletionWindow(parseInt(e.target.value))
}}
/>
</Box>
</Card>
)}
</Box> </Box>
{!['once', 'no_repeat'].includes(frequencyType) && ( {!['once', 'no_repeat'].includes(frequencyType) && (
<Box mt={2}> <Box mt={2}>

View file

@ -36,6 +36,7 @@ import { Divider } from '@mui/material'
import moment from 'moment' import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { notInCompletionWindow } from '../../utils/Chores.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import { import {
GetAllUsers, GetAllUsers,
@ -563,7 +564,7 @@ const ChoreView = () => {
fullWidth fullWidth
size='lg' size='lg'
onClick={handleTaskCompletion} onClick={handleTaskCompletion}
disabled={isPendingCompletion} disabled={isPendingCompletion || notInCompletionWindow(chore)}
color={isPendingCompletion ? 'danger' : 'success'} color={isPendingCompletion ? 'danger' : 'success'}
startDecorator={<Check />} startDecorator={<Check />}
sx={{ sx={{

View file

@ -43,6 +43,7 @@ import moment from 'moment'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { UserContext } from '../../contexts/UserContext' import { UserContext } from '../../contexts/UserContext'
import { notInCompletionWindow } from '../../utils/Chores.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import { import {
ArchiveChore, ArchiveChore,
@ -412,7 +413,6 @@ const ChoreCard = ({
} }
return name return name
} }
return ( return (
<Box key={chore.id + '-box'}> <Box key={chore.id + '-box'}>
<Chip <Chip
@ -571,7 +571,7 @@ const ChoreCard = ({
variant='solid' variant='solid'
color='success' color='success'
onClick={handleTaskCompletion} onClick={handleTaskCompletion}
disabled={isPendingCompletion} disabled={isPendingCompletion || notInCompletionWindow(chore)}
sx={{ sx={{
borderRadius: '50%', borderRadius: '50%',
minWidth: 50, minWidth: 50,

View file

@ -14,14 +14,12 @@ import {
import moment from 'moment' import moment from 'moment'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import { API_URL } from '../../Config'
import { import {
DeleteChoreHistory, DeleteChoreHistory,
GetAllCircleMembers, GetAllCircleMembers,
GetChoreHistory, GetChoreHistory,
UpdateChoreHistory, UpdateChoreHistory,
} from '../../utils/Fetcher' } from '../../utils/Fetcher'
import { Fetch } from '../../utils/TokenManager'
import LoadingComponent from '../components/Loading' import LoadingComponent from '../components/Loading'
import EditHistoryModal from '../Modals/EditHistoryModal' import EditHistoryModal from '../Modals/EditHistoryModal'
import HistoryCard from './HistoryCard' import HistoryCard from './HistoryCard'
@ -42,7 +40,7 @@ const ChoreHistory = () => {
Promise.all([ Promise.all([
GetChoreHistory(choreId).then(res => res.json()), GetChoreHistory(choreId).then(res => res.json()),
GetAllCircleMembers().then(res => res.json()), GetAllCircleMembers(),
]) ])
.then(([historyData, usersData]) => { .then(([historyData, usersData]) => {
setChoresHistory(historyData.res) setChoresHistory(historyData.res)

View file

@ -54,9 +54,7 @@ const Settings = () => {
setCircleMemberRequests(data.res ? data.res : []) setCircleMemberRequests(data.res ? data.res : [])
}) })
}) })
GetAllCircleMembers() GetAllCircleMembers().then(data => {
.then(res => res.json())
.then(data => {
setCircleMembers(data.res ? data.res : []) setCircleMembers(data.res ? data.res : [])
}) })
}, []) }, [])

View file

@ -3,14 +3,16 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CircleIcon from '@mui/icons-material/Circle' import CircleIcon from '@mui/icons-material/Circle'
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts' import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'
import { Toll } from '@mui/icons-material' import { EventBusy, Toll } from '@mui/icons-material'
import { import {
Box, Box,
Button,
Card, Card,
Chip, Chip,
Container, Container,
Divider, Divider,
Grid, Grid,
Link,
Stack, Stack,
Tab, Tab,
TabList, TabList,
@ -173,7 +175,7 @@ const UserActivites = () => {
data: choresHistory, data: choresHistory,
isChoresHistoryLoading, isChoresHistoryLoading,
handleLimitChange: refetchHistory, handleLimitChange: refetchHistory,
} = useChoresHistory(tabValue ? tabValue : 30) } = useChoresHistory(tabValue ? tabValue : 30, false)
useEffect(() => { useEffect(() => {
if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) { if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) {
const enrichedHistory = choresHistory.res.map(item => { const enrichedHistory = choresHistory.res.map(item => {
@ -306,6 +308,40 @@ const UserActivites = () => {
}, },
} }
if (!choresData.res?.length > 0 || !choresHistory?.res?.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>
)
}
return ( return (
<Container <Container
maxWidth='md' maxWidth='md'

View file

@ -0,0 +1,465 @@
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
XAxis,
YAxis,
} from 'recharts'
import { Toll } from '@mui/icons-material'
import {
Avatar,
Box,
Card,
Chip,
Container,
Option,
Select,
Tab,
TabList,
Tabs,
Typography,
} from '@mui/joy'
import { useContext, useEffect, useState } from 'react'
import { UserContext } from '../../contexts/UserContext.js'
import LoadingComponent from '../components/Loading.jsx'
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
import { useCircleMembers } from '../../queries/UserQueries.jsx'
const UserPoints = () => {
const [tabValue, setTabValue] = useState(7)
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
useCircleMembers()
const {
data: choresHistoryData,
isLoading: isChoresHistoryLoading,
handleLimitChange: handleChoresHistoryLimitChange,
} = useChoresHistory(7)
const { userProfile } = useContext(UserContext)
const [selectedUser, setSelectedUser] = useState(userProfile?.id)
const [circleUsers, setCircleUsers] = useState([])
const [selectedHistory, setSelectedHistory] = useState([])
const [userPointsBarChartData, setUserPointsBarChartData] = useState([])
const [choresHistory, setChoresHistory] = useState([])
useEffect(() => {
if (circleMembersData && choresHistoryData && userProfile) {
setCircleUsers(circleMembersData.res)
setSelectedHistory(generateWeeklySummary(choresHistory, userProfile?.id))
}
}, [circleMembersData, choresHistoryData])
useEffect(() => {
if (choresHistoryData) {
var history
if (tabValue === 7) {
history = generateWeeklySummary(choresHistoryData.res, selectedUser)
} else if (tabValue === 30) {
history = generateMonthSummary(choresHistoryData.res, selectedUser)
} else if (tabValue === 6 * 30) {
history = generateMonthlySummary(choresHistoryData.res, selectedUser)
} else {
history = generateYearlySummary(choresHistoryData.res, selectedUser)
}
setSelectedHistory(history)
}
}, [selectedUser, choresHistoryData])
useEffect(() => {
setSelectedUser(userProfile?.id)
}, [userProfile])
const generateUserPointsHistory = history => {
const userPoints = {}
for (let i = 0; i < history.length; i++) {
const chore = history[i]
if (!userPoints[chore.completedBy]) {
userPoints[chore.completedBy] = chore.points ? chore.points : 0
} else {
userPoints[chore.completedBy] += chore.points ? chore.points : 0
}
}
return userPoints
}
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: currentDate.toLocaleString('en-US', { weekday: 'short' }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const dayName = new Date(chore.completedAt).toLocaleString('en-US', {
weekday: 'short',
})
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: currentDate.toLocaleString('en-US', { day: 'numeric' }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const dayName = new Date(chore.completedAt).toLocaleString('en-US', {
day: 'numeric',
})
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: currentMonth.toLocaleString('en-US', { month: 'short' }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const monthName = new Date(chore.completedAt).toLocaleString('en-US', {
month: 'short',
})
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: currentYear.toLocaleString('en-US', { year: 'numeric' }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
const yearName = new Date(chore.completedAt).toLocaleString('en-US', {
year: 'numeric',
})
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) {
return <LoadingComponent />
}
return (
<Container
maxWidth='md'
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Box
sx={{
mb: 4,
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
>
<Typography level='h4'>Points Overview</Typography>
<Box
sx={{
gap: 1,
my: 2,
}}
>
<Select
sx={{
width: 200,
}}
variant='soft'
label='User'
value={selectedUser}
onChange={(e, selected) => {
setSelectedUser(selected)
setSelectedHistory(generateWeeklySummary(choresHistory, selected))
}}
renderValue={selected => (
<Typography
startDecorator={
<Avatar color='primary' m={0} size='sm'>
{
circleUsers.find(user => user.userId === selectedUser)
?.displayName[0]
}
</Avatar>
}
>
{
circleUsers.find(user => user.userId === selectedUser)
?.displayName
}
</Typography>
)}
>
{circleUsers.map(user => (
<Option key={user.userId} value={user.userId}>
<Typography>{user.displayName}</Typography>
<Chip
color='success'
size='sm'
variant='soft'
startDecorator={<Toll />}
>
{user.points - user.pointsRedeemed}
</Chip>
</Option>
))}
</Select>
</Box>
<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>
<Typography level='h4'>Points History</Typography>
<Box
sx={{
// center vertically:
display: 'flex',
justifyContent: 'left',
gap: 1,
}}
>
<Tabs
onChange={(e, tabValue) => {
setTabValue(tabValue)
handleChoresHistoryLimitChange(tabValue)
}}
defaultValue={tabValue}
sx={{
py: 0.5,
borderRadius: 16,
maxWidth: 400,
mb: 1,
}}
>
<TabList
disableUnderline
sx={{
borderRadius: 16,
backgroundColor: 'background.paper',
boxShadow: 1,
justifyContent: 'space-evenly',
}}
>
{[
{ label: '7 Days', value: 7 },
// { label: '3 Month', value: 30 },
{ label: '6 Months', value: 6 * 30 },
{ label: 'All Time', value: 90 },
].map((tab, index) => (
<Tab
key={index}
sx={{
borderRadius: 16,
color: 'text.secondary',
'&.Mui-selected': {
color: 'text.primary',
backgroundColor: 'primary.light',
},
}}
disableIndicator
value={tab.value}
>
{tab.label}
</Tab>
))}
</TabList>
</Tabs>
</Box>
<Box
sx={{
// resposive width based on parent available space:
width: '100%',
display: 'flex',
justifyContent: 'left',
gap: 1,
}}
>
{[
{
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>
))}
</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]}
>
{/* Rounded top corners, blue fill, set bar width */}
{/* Add a slightly darker top section to the 'Jul' bar */}
</Bar>
</BarChart>
</ResponsiveContainer>
</Box>
</Box>
</Container>
)
}
export default UserPoints

View file

@ -9,6 +9,7 @@ import {
Message, Message,
SettingsOutlined, SettingsOutlined,
ShareOutlined, ShareOutlined,
Toll,
Widgets, Widgets,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
@ -43,15 +44,20 @@ const links = [
label: 'Things', label: 'Things',
icon: <Widgets />, icon: <Widgets />,
}, },
{
to: 'labels',
label: 'Labels',
icon: <ListAlt />,
},
{ {
to: 'activities', to: 'activities',
label: 'Activities', label: 'Activities',
icon: <History />, icon: <History />,
}, },
{ {
to: 'labels', to: 'points',
label: 'Labels', label: 'Points',
icon: <ListAlt />, icon: <Toll />,
}, },
{ {
to: '/settings#sharing', to: '/settings#sharing',