Merge branch 'dev'

This commit is contained in:
Mo Tarbin 2024-07-16 19:37:59 -04:00
commit 8e42e59a80
13 changed files with 610 additions and 260 deletions

View file

@ -73,6 +73,16 @@ const MarkChoreComplete = (id, note, completedDate) => {
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
} }
const SkipChore = id => {
return Fetch(`${API_URL}/chores/${id}/skip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
}
const CreateChore = chore => { const CreateChore = chore => {
return Fetch(`${API_URL}/chores/`, { return Fetch(`${API_URL}/chores/`, {
method: 'POST', method: 'POST',
@ -277,6 +287,7 @@ export {
SaveChore, SaveChore,
SaveThing, SaveThing,
signUp, signUp,
SkipChore,
UpdateThingState, UpdateThingState,
UpdateUserDetails, UpdateUserDetails,
} }

View file

@ -3,48 +3,53 @@ import {
CancelScheduleSend, CancelScheduleSend,
Check, Check,
Checklist, Checklist,
Note, History,
PeopleAlt, PeopleAlt,
Person, Person,
SwitchAccessShortcut,
Timelapse,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
Box, Box,
Button, Button,
Card, Card,
Checkbox, Checkbox,
Chip,
Container, Container,
FormControl, FormControl,
Grid, Grid,
Input, Input,
ListItem, ListItem,
ListItemContent, ListItemContent,
ListItemDecorator,
Sheet, Sheet,
Snackbar, Snackbar,
styled, styled,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { Divider } from '@mui/material'
import moment from 'moment' import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useParams, useSearchParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { import {
GetAllUsers, GetAllUsers,
GetChoreDetailById, GetChoreDetailById,
MarkChoreComplete, MarkChoreComplete,
SkipChore,
} from '../../utils/Fetcher' } from '../../utils/Fetcher'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
const IconCard = styled('div')({ const IconCard = styled('div')({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#f0f0f0', // Adjust the background color as needed backgroundColor: '#f0f0f0', // Adjust the background color as needed
borderRadius: '50%', borderRadius: '50%',
minWidth: '50px', minWidth: '40px',
height: '50px', height: '40px',
marginRight: '16px', marginRight: '16px',
}) })
const ChoreView = () => { const ChoreView = () => {
const [chore, setChore] = useState({}) const [chore, setChore] = useState({})
const navigate = useNavigate()
const [performers, setPerformers] = useState([]) const [performers, setPerformers] = useState([])
const [infoCards, setInfoCards] = useState([]) const [infoCards, setInfoCards] = useState([])
const { choreId } = useParams() const { choreId } = useParams()
@ -56,6 +61,8 @@ const ChoreView = () => {
const [timeoutId, setTimeoutId] = useState(null) const [timeoutId, setTimeoutId] = useState(null)
const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null) const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null)
const [completedDate, setCompletedDate] = useState(null) const [completedDate, setCompletedDate] = useState(null)
const [confirmModelConfig, setConfirmModelConfig] = useState({})
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
GetChoreDetailById(choreId).then(resp => { GetChoreDetailById(choreId).then(resp => {
@ -85,20 +92,20 @@ const ChoreView = () => {
const generateInfoCards = chore => { const generateInfoCards = chore => {
const cards = [ const cards = [
{ {
icon: <CalendarMonth />, size: 6,
text: 'Due Date',
subtext: moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A'),
},
{
icon: <PeopleAlt />, icon: <PeopleAlt />,
text: 'Assigned To', text: 'Assigned To',
subtext: performers.find(p => p.id === chore.assignedTo)?.displayName, subtext: performers.find(p => p.id === chore.assignedTo)?.displayName,
}, },
{ {
icon: <Person />, size: 6,
text: 'Created By', icon: <CalendarMonth />,
subtext: performers.find(p => p.id === chore.createdBy)?.displayName, text: 'Due Date',
subtext: chore.nextDueDate
? moment(chore.nextDueDate).fromNow()
: 'N/A',
}, },
// { // {
// icon: <TextFields />, // icon: <TextFields />,
// text: 'Frequency', // text: 'Frequency',
@ -107,35 +114,42 @@ const ChoreView = () => {
// chore.frequencyType.slice(1), // chore.frequencyType.slice(1),
// }, // },
{ {
size: 6,
icon: <Checklist />, icon: <Checklist />,
text: 'Total Completed', text: 'Total Completed',
subtext: `${chore.totalCompletedCount}`, subtext: `${chore.totalCompletedCount} times`,
}, },
// { {
// icon: <Timelapse />, size: 6,
// text: 'Last Completed', icon: <Timelapse />,
// subtext: text: 'Last Completed',
subtext:
// chore.lastCompletedDate && // chore.lastCompletedDate &&
// moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A'), // moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A'),
// }, chore.lastCompletedDate && moment(chore.lastCompletedDate).fromNow(),
},
{ {
size: 6,
icon: <Person />, icon: <Person />,
text: 'Last Completed', text: 'Last Performer',
subtext: chore.lastCompletedDate subtext: chore.lastCompletedDate
? `${ ? `${
chore.lastCompletedDate &&
moment(chore.lastCompletedDate).fromNow()
// moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A'))
}(${
performers.find(p => p.id === chore.lastCompletedBy)?.displayName performers.find(p => p.id === chore.lastCompletedBy)?.displayName
})` }`
: 'Never', : '--',
}, },
{ {
icon: <Note />, size: 6,
text: 'Recent Note', icon: <Person />,
subtext: chore.notes || '--', text: 'Created By',
subtext: performers.find(p => p.id === chore.createdBy)?.displayName,
}, },
// {
// size: 12,
// icon: <Note />,
// text: 'Recent Note',
// subtext: chore.notes || '--',
// },
] ]
setInfoCards(cards) setInfoCards(cards)
} }
@ -184,7 +198,16 @@ const ChoreView = () => {
setTimeoutId(id) setTimeoutId(id)
} }
const handleSkippingTask = () => {
SkipChore(choreId).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
setChore(newChore)
})
}
})
}
return ( return (
<Container <Container
maxWidth='sm' maxWidth='sm'
@ -195,46 +218,88 @@ const ChoreView = () => {
justifyContent: 'space-between', justifyContent: 'space-between',
}} }}
> >
<Box> <Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
}}
>
<Typography <Typography
level='h3' level='h3'
textAlign={'center'} // textAlign={'center'}
sx={{ sx={{
mt: 2, mt: 1,
mb: 4, mb: 0.5,
}} }}
> >
{chore.name} {chore.name}
</Typography> </Typography>
<Chip startDecorator={<CalendarMonth />} size='lg' sx={{ mb: 4 }}>
{chore.nextDueDate
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
: 'N/A'}
</Chip>
</Box>
<Box>
<Typography level='title-md' sx={{ mb: 0.5 }}>
Details
</Typography>
<Sheet
sx={{
mb: 1,
borderRadius: 'lg',
p: 2,
}}
variant='outlined'
>
<Grid container spacing={1}> <Grid container spacing={1}>
{infoCards.map((info, index) => ( {infoCards.map((detail, index) => (
<Grid key={index} item xs={12} sm={6}> <Grid item xs={4} key={index}>
<Sheet sx={{ mb: 1, borderRadius: 'md', p: 1, boxShadow: 'sm' }}> {/* divider between the list items: */}
<ListItem>
<ListItemDecorator> <ListItem key={index}>
<IconCard>{info.icon}</IconCard>
</ListItemDecorator>
<ListItemContent> <ListItemContent>
<Typography level='body1' sx={{ fontWeight: 'md' }}> <Typography level='body-xs' sx={{ fontWeight: 'md' }}>
{info.text} {detail.text}
</Typography>
<Typography level='body1' color='text.tertiary'>
{info.subtext ? info.subtext : '--'}
</Typography> </Typography>
<Chip
color='primary'
size='md'
startDecorator={detail.icon}
>
{detail.subtext ? detail.subtext : '--'}
</Chip>
</ListItemContent> </ListItemContent>
</ListItem> </ListItem>
</Sheet>
</Grid> </Grid>
))} ))}
</Grid> </Grid>
</Sheet>
{chore.notes && (
<>
<Typography level='title-md' sx={{ mb: 1 }}>
Previous note:
</Typography>
<Sheet variant='outlined' sx={{ p: 2, borderRadius: 'lg' }}>
<Typography level='body-md' sx={{ mb: 1 }}>
{chore.notes || '--'}
</Typography>
</Sheet>
</>
)}
</Box> </Box>
{/* <Divider {/* <Divider
sx={{ sx={{
my: 2, my: 2,
}} }}
/> */} /> */}
<Typography level='title-md' sx={{ mt: 1 }}>
Actions
</Typography>
<Card <Card
sx={{ sx={{
p: 2, p: 2,
@ -243,7 +308,44 @@ const ChoreView = () => {
mt: 2, mt: 2,
}} }}
> >
<Typography level='title-md'>Additional Notes</Typography> <Typography level='body-md' sx={{ mb: 1 }}>
Complete the task
</Typography>
<FormControl size='sm'>
<Checkbox
defaultChecked={note !== null}
checked={note !== null}
value={note !== null}
size='lg'
onChange={e => {
if (e.target.checked) {
setNote('')
} else {
setNote(null)
}
}}
overlay
sx={
{
// my: 1,
}
}
label={
<Typography
level='body-sm'
sx={{
// center vertically
display: 'flex',
alignItems: 'center',
}}
>
Add Additional Notes
</Typography>
}
/>
</FormControl>
{note !== null && (
<Input <Input
fullWidth fullWidth
multiline multiline
@ -259,11 +361,12 @@ const ChoreView = () => {
}} }}
size='md' size='md'
sx={{ sx={{
my: 1, mb: 1,
}} }}
/> />
)}
<FormControl size='sm' sx={{ width: 400 }}> <FormControl size='sm'>
<Checkbox <Checkbox
defaultChecked={completedDate !== null} defaultChecked={completedDate !== null}
checked={completedDate !== null} checked={completedDate !== null}
@ -279,10 +382,23 @@ const ChoreView = () => {
} }
}} }}
overlay overlay
sx={
{
// my: 1,
}
}
label={
<Typography
level='body-sm'
sx={{ sx={{
my: 1, // center vertically
display: 'flex',
alignItems: 'center',
}} }}
label={<Typography level='body2'>Set completion date</Typography>} >
Specify completion date
</Typography>
}
/> />
</FormControl> </FormControl>
{completedDate !== null && ( {completedDate !== null && (
@ -295,22 +411,10 @@ const ChoreView = () => {
}} }}
/> />
)} )}
{completedDate === null && (
// placeholder for the completion date with margin:
<Box
sx={{
height: 56,
}}
/>
)}
<Button <Button
fullWidth fullWidth
size='lg' size='lg'
sx={{
height: 50,
mb: 2,
}}
onClick={handleTaskCompletion} onClick={handleTaskCompletion}
disabled={isPendingCompletion} disabled={isPendingCompletion}
color={isPendingCompletion ? 'danger' : 'success'} color={isPendingCompletion ? 'danger' : 'success'}
@ -318,23 +422,54 @@ const ChoreView = () => {
> >
<Box>Mark as done</Box> <Box>Mark as done</Box>
</Button> </Button>
{/* <Button <Divider sx={{ my: 0.5 }}>or</Divider>
<Box
sx={{ sx={{
borderRadius: '32px', display: 'flex',
mt: 1, flexDirection: 'row',
height: 50, gap: 1,
zIndex: 1, alignContent: 'center',
justifyContent: 'center',
}} }}
onClick={() => {
Navigate('/my/chores')
}}
color={isPendingCompletion ? 'danger' : 'success'}
startDecorator={isPendingCompletion ? <Close /> : <Check />}
fullWidth
> >
<Box>Mark as {isPendingCompletion ? 'completed' : 'done'}</Box> <Button
</Button> */} fullWidth
</Card> size='lg'
onClick={() => {
setConfirmModelConfig({
isOpen: true,
title: 'Skip Task',
message: 'Are you sure you want to skip this task?',
confirmText: 'Skip',
cancelText: 'Cancel',
onClose: confirmed => {
if (confirmed) {
handleSkippingTask()
}
setConfirmModelConfig({})
},
})
}}
startDecorator={<SwitchAccessShortcut />}
>
<Box>Skip</Box>
</Button>
<Button
startDecorator={<History />}
size='lg'
color='primary'
variant='outlined'
fullWidth
onClick={() => {
navigate(`/chores/${choreId}/history`)
}}
>
History
</Button>
</Box>
<Snackbar <Snackbar
open={isPendingCompletion} open={isPendingCompletion}
@ -361,6 +496,8 @@ const ChoreView = () => {
Task will be marked as completed in {secondsLeftToCancel} seconds Task will be marked as completed in {secondsLeftToCancel} seconds
</Typography> </Typography>
</Snackbar> </Snackbar>
<ConfirmationModal config={confirmModelConfig} />
</Card>
</Container> </Container>
) )
} }

View file

@ -509,7 +509,7 @@ const RepeatSection = ({
}} }}
> >
Is this something that should be done when a thing state changes?{' '} Is this something that should be done when a thing state changes?{' '}
{!isPlusAccount(userProfile) && ( {userProfile && !isPlusAccount(userProfile) && (
<Chip variant='soft' color='warning'> <Chip variant='soft' color='warning'>
Not available in Basic Plan Not available in Basic Plan
</Chip> </Chip>

View file

@ -17,6 +17,7 @@ import {
SwitchAccessShortcut, SwitchAccessShortcut,
TimesOneMobiledata, TimesOneMobiledata,
Update, Update,
ViewCarousel,
Webhook, Webhook,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
@ -38,7 +39,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 { API_URL } from '../../Config' import { API_URL } from '../../Config'
import { MarkChoreComplete } from '../../utils/Fetcher' import { MarkChoreComplete, SkipChore } from '../../utils/Fetcher'
import { Fetch } from '../../utils/TokenManager' import { Fetch } from '../../utils/TokenManager'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import DateModal from '../Modals/Inputs/DateModal' import DateModal from '../Modals/Inputs/DateModal'
@ -107,6 +108,9 @@ const ChoreCard = ({
const handleEdit = () => { const handleEdit = () => {
navigate(`/chores/${chore.id}/edit`) navigate(`/chores/${chore.id}/edit`)
} }
const handleView = () => {
navigate(`/chores/${chore.id}`)
}
const handleDelete = () => { const handleDelete = () => {
setConfirmModelConfig({ setConfirmModelConfig({
isOpen: true, isOpen: true,
@ -521,13 +525,7 @@ const ChoreCard = ({
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
Fetch(`${API_URL}/chores/${chore.id}/skip`, { SkipChore(chore.id).then(response => {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}).then(response => {
if (response.ok) { if (response.ok) {
response.json().then(data => { response.json().then(data => {
const newChore = data.res const newChore = data.res
@ -585,6 +583,10 @@ const ChoreCard = ({
<Edit /> <Edit />
Edit Edit
</MenuItem> </MenuItem>
<MenuItem onClick={handleView}>
<ViewCarousel />
View
</MenuItem>
<MenuItem onClick={handleDelete} color='danger'> <MenuItem onClick={handleDelete} color='danger'>
<Delete /> <Delete />
Delete Delete

View file

@ -3,7 +3,6 @@ import {
Badge, Badge,
Box, Box,
Checkbox, Checkbox,
CircularProgress,
Container, Container,
IconButton, IconButton,
Input, Input,
@ -18,8 +17,8 @@ import Fuse from 'fuse.js'
import { useContext, useEffect, useRef, useState } from 'react' import { useContext, useEffect, useRef, useState } 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 Logo from '../../Logo'
import { GetAllUsers, GetChores, GetUserProfile } from '../../utils/Fetcher' import { GetAllUsers, GetChores, GetUserProfile } from '../../utils/Fetcher'
import LoadingComponent from '../components/Loading'
import ChoreCard from './ChoreCard' import ChoreCard from './ChoreCard'
const MyChores = () => { const MyChores = () => {
@ -184,18 +183,7 @@ const MyChores = () => {
} }
if (userProfile === null) { if (userProfile === null) {
return ( return <LoadingComponent />
<Container className='flex h-full items-center justify-center'>
<Box className='flex flex-col items-center justify-center'>
<CircularProgress
color='success'
sx={{ '--CircularProgress-size': '200px' }}
>
<Logo />
</CircularProgress>
</Box>
</Container>
)
} }
return ( return (

View file

@ -1,15 +1,13 @@
import { Checklist, EventBusy, Timelapse } from '@mui/icons-material' import { Checklist, EventBusy, Group, Timelapse } from '@mui/icons-material'
import { import {
Avatar, Avatar,
Box,
Button, Button,
CircularProgress, Chip,
Container, Container,
Grid, Grid,
List, List,
ListItem, ListItem,
ListItemContent, ListItemContent,
ListItemDecorator,
Sheet, Sheet,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
@ -19,6 +17,7 @@ import { Link, useParams } from 'react-router-dom'
import { API_URL } from '../../Config' import { API_URL } from '../../Config'
import { GetAllCircleMembers } from '../../utils/Fetcher' import { GetAllCircleMembers } from '../../utils/Fetcher'
import { Fetch } from '../../utils/TokenManager' import { Fetch } from '../../utils/TokenManager'
import LoadingComponent from '../components/Loading'
import HistoryCard from './HistoryCard' import HistoryCard from './HistoryCard'
const ChoreHistory = () => { const ChoreHistory = () => {
@ -92,59 +91,49 @@ const ChoreHistory = () => {
const historyInfo = [ const historyInfo = [
{ {
icon: ( icon: <Checklist />,
<Avatar> text: 'Total Completed',
<Checklist /> subtext: `${histories.length} times`,
</Avatar>
),
text: `${histories.length} completed`,
subtext: `${Object.keys(userHistories).length} users contributed`,
}, },
{ {
icon: ( icon: <Timelapse />,
<Avatar> text: 'Usually Within',
<Timelapse /> subtext: moment.duration(averageDelayMoment).humanize(),
</Avatar>
),
text: `Completed within ${moment
.duration(averageDelayMoment)
.humanize()}`,
subtext: `Maximum delay was ${moment
.duration(maxDelayMoment)
.humanize()}`,
}, },
{ {
icon: <Avatar></Avatar>, icon: <Timelapse />,
text: `${ text: 'Maximum Delay',
subtext: moment.duration(maxDelayMoment).humanize(),
},
{
icon: <Avatar />,
text: ' Completed Most',
subtext: `${
performers.find(p => p.userId === Number(userCompletedByMost)) performers.find(p => p.userId === Number(userCompletedByMost))
?.displayName ?.displayName
} completed most`, } `,
subtext: `${userHistories[userCompletedByMost]} time/s`, },
// contributes:
{
icon: <Group />,
text: 'Total Performers',
subtext: `${Object.keys(userHistories).length} users`,
},
{
icon: <Avatar />,
text: 'Last Completed',
subtext: `${
performers.find(p => p.userId === Number(histories[0].completedBy))
?.displayName
}`,
}, },
] ]
if (userCompletedByLeast !== userCompletedByMost) {
historyInfo.push({
icon: (
<Avatar>
{
performers.find(p => p.userId === userCompletedByLeast)
?.displayName
}
</Avatar>
),
text: `${
performers.find(p => p.userId === Number(userCompletedByLeast))
.displayName
} completed least`,
subtext: `${userHistories[userCompletedByLeast]} time/s`,
})
}
setHistoryInfo(historyInfo) setHistoryInfo(historyInfo)
} }
if (isLoading) { if (isLoading) {
return <CircularProgress /> // Show loading indicator return <LoadingComponent />
} }
if (!choreHistory.length) { if (!choreHistory.length) {
return ( return (
@ -184,50 +173,43 @@ const ChoreHistory = () => {
return ( return (
<Container maxWidth='md'> <Container maxWidth='md'>
<Typography level='h3' mb={1.5}> <Typography level='title-md' mb={1.5}>
Summary: Summary:
</Typography> </Typography>
{/* <Sheet sx={{ mb: 1, borderRadius: 'sm', p: 2, boxShadow: 'md' }}> <Sheet
<ListItem sx={{ gap: 1.5 }}> // sx={{
<ListItemDecorator> // mb: 1,
<Avatar> // borderRadius: 'lg',
<AccountCircle /> // p: 2,
</Avatar> // }}
</ListItemDecorator> sx={{ borderRadius: 'sm', p: 2 }}
<ListItemContent> variant='outlined'
<Typography level='body1' sx={{ fontWeight: 'md' }}> >
{choreHistory.length} completed <Grid container spacing={1}>
</Typography>
<Typography level='body2' color='text.tertiary'>
{Object.keys(userHistory).length} users contributed
</Typography>
</ListItemContent>
</ListItem>
</Sheet> */}
<Grid container>
{historyInfo.map((info, index) => ( {historyInfo.map((info, index) => (
<Grid key={index} item xs={12} sm={6}> <Grid item xs={4} key={index}>
<Sheet sx={{ mb: 1, borderRadius: 'sm', p: 2, boxShadow: 'md' }}> {/* divider between the list items: */}
<ListItem sx={{ gap: 1.5 }}>
<ListItemDecorator>{info.icon}</ListItemDecorator> <ListItem key={index}>
<ListItemContent> <ListItemContent>
<Typography level='body1' sx={{ fontWeight: 'md' }}> <Typography level='body-xs' sx={{ fontWeight: 'md' }}>
{info.text} {info.text}
</Typography> </Typography>
<Typography level='body1' color='text.tertiary'> <Chip color='primary' size='md' startDecorator={info.icon}>
{info.subtext} {info.subtext ? info.subtext : '--'}
</Typography> </Chip>
</ListItemContent> </ListItemContent>
</ListItem> </ListItem>
</Sheet>
</Grid> </Grid>
))} ))}
</Grid> </Grid>
</Sheet>
{/* User History Cards */} {/* User History Cards */}
<Typography level='h3' my={1.5}> <Typography level='title-md' my={1.5}>
History: History:
</Typography> </Typography>
<Box sx={{ borderRadius: 'sm', p: 2, boxShadow: 'md' }}> <Sheet sx={{ borderRadius: 'sm', p: 2, boxShadow: 'md' }}>
{/* Chore History List (Updated Style) */} {/* Chore History List (Updated Style) */}
<List sx={{ p: 0 }}> <List sx={{ p: 0 }}>
@ -241,7 +223,7 @@ const ChoreHistory = () => {
/> />
))} ))}
</List> </List>
</Box> </Sheet>
</Container> </Container>
) )
} }

View file

@ -1,3 +1,4 @@
import { CalendarViewDay, Check, Timelapse } from '@mui/icons-material'
import { import {
Avatar, Avatar,
Box, Box,
@ -30,6 +31,50 @@ const HistoryCard = ({ allHistory, performers, historyEntry, index }) => {
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}` return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
} }
const getCompletedChip = historyEntry => {
var text = 'No Due Date'
var color = 'info'
var icon = <CalendarViewDay />
// if completed few hours +-6 hours
if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 &&
historyEntry.completedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6
) {
text = 'On Time'
color = 'success'
icon = <Check />
} else if (
historyEntry.dueDate &&
historyEntry.completedAt < historyEntry.dueDate
) {
text = 'On Time'
color = 'success'
icon = <Check />
}
// if completed after due date then it's late
else if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate
) {
text = 'Late'
color = 'warning'
icon = <Timelapse />
} else {
text = 'No Due Date'
color = 'info'
icon = <CalendarViewDay />
}
return (
<Chip startDecorator={icon} color={color}>
{text}
</Chip>
)
}
return ( return (
<> <>
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}> <ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
@ -55,13 +100,7 @@ const HistoryCard = ({ allHistory, performers, historyEntry, index }) => {
<Typography level='body1' sx={{ fontWeight: 'md' }}> <Typography level='body1' sx={{ fontWeight: 'md' }}>
{moment(historyEntry.completedAt).format('ddd MM/DD/yyyy HH:mm')} {moment(historyEntry.completedAt).format('ddd MM/DD/yyyy HH:mm')}
</Typography> </Typography>
{getCompletedChip(historyEntry)}
<Chip>
{historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate
? 'Late'
: 'On Time'}
</Chip>
</Box> </Box>
<Typography level='body2' color='text.tertiary'> <Typography level='body2' color='text.tertiary'>
<Chip> <Chip>

View file

@ -7,7 +7,7 @@ const DemoHistory = () => {
{ {
id: 32, id: 32,
choreId: 12, choreId: 12,
completedAt: moment().format(), completedAt: moment().hour(4).format(),
completedBy: 1, completedBy: 1,
assignedTo: 1, assignedTo: 1,
notes: null, notes: null,
@ -25,8 +25,8 @@ const DemoHistory = () => {
{ {
id: 31, id: 31,
choreId: 12, choreId: 12,
completedAt: moment().day(-10).format(), completedAt: moment().day(-10).hour(1).format(),
completedBy: 1, completedBy: 2,
assignedTo: 1, assignedTo: 1,
notes: null, notes: null,
dueDate: moment().day(-10).format(), dueDate: moment().day(-10).format(),

View file

@ -25,44 +25,39 @@ const FeatureIcon = styled('div')({
const CardData = [ const CardData = [
{ {
title: 'Open Source & Transparent', title: 'Open Source & Transparent',
headline: 'Built for the Community',
description: description:
'Donetick is a community-driven, open-source project. Contribute, customize, and make task management truly yours.', 'Donetick is open source software. You can view, modify, and contribute to the code on GitHub.',
icon: CodeRounded, icon: CodeRounded,
}, },
{ {
title: 'Circles: Your Task Hub', title: 'Circles: Your Task Hub',
headline: 'Share & Conquer Together',
description: description:
'Create circles for your family, friends, or team. Easily share tasks and track progress within each group.', 'build with sharing in mind. invite other to the circle and you can assign tasks to each other. and only see the tasks the should be shared',
icon: GroupRounded, icon: GroupRounded,
}, },
{ {
title: 'Track Your Progress', title: 'Track Your Progress',
headline: "See Who's Done What",
description: description:
'View a history of task completion for each member of your circles. Celebrate successes and stay on top of your goals.', 'View a history of completed tasks. or use things to track simply things!',
icon: HistoryRounded, icon: HistoryRounded,
}, },
{ {
title: 'Automated Chore Scheduling', title: 'Automated Task Scheduling',
headline: 'Fully Customizable Recurring Tasks',
description: description:
'Set up chores to repeat daily, weekly, or monthly. Donetick will automatically assign and track each task for you.', 'Set up Tasks to repeat daily, weekly, or monthly, or maybe specifc day in specifc months? Donetick have a flexible scheduling system',
icon: AutoAwesomeMosaicOutlined, icon: AutoAwesomeMosaicOutlined,
}, },
{ {
title: 'Automated Task Assignment', title: 'Automated Task Assignment',
headline: 'Share Responsibilities Equally',
description: description:
'can automatically assigns tasks to each member of your circle. Randomly or based on past completion.', 'For shared tasks, Donetick can randomly rotate assignments or choose based on last completion or least assigned.',
icon: AutoAwesomeRounded, icon: AutoAwesomeRounded,
}, },
{ {
title: 'Integrations & Webhooks', title: 'Integrations & Webhooks',
headline: 'API & 3rd Party Integrations',
description: description:
'Connect Donetick with your favorite apps and services. Trigger tasks based on events from other platforms.', 'Donetick can update things programmatically with API call. you can update things from other services like IFTTT, Homeassistant or even your own service',
icon: Webhook, icon: Webhook,
}, },
] ]
@ -80,7 +75,7 @@ function Feature2({ icon: Icon, title, headline, description, index }) {
<FeatureIcon> <FeatureIcon>
<Icon <Icon
color='primary' color='primary'
style={{ Width: '30px', height: '30px' }} style={{ Width: '30px', height: '30px', fontSize: '30px' }}
stroke={1.5} stroke={1.5}
/> />
</FeatureIcon> </FeatureIcon>
@ -106,7 +101,7 @@ function FeaturesSection() {
<Feature2 <Feature2
icon={feature.icon} icon={feature.icon}
title={feature.title} title={feature.title}
headline={feature.headline} // headline={feature.headline}
description={feature.description} description={feature.description}
index={index} index={index}
key={index} key={index}
@ -128,7 +123,7 @@ function FeaturesSection() {
</Container> </Container>
<Typography level='h4' mt={2} mb={4}> <Typography level='h4' mt={2} mb={4}>
Features Overview Why Donetick?
</Typography> </Typography>
<Container maxWidth={'lg'} sx={{ mb: 8 }}></Container> <Container maxWidth={'lg'} sx={{ mb: 8 }}></Container>

View file

@ -0,0 +1,127 @@
import LogoSVG from '@/assets/logo.svg'
import { Card, Grid } from '@mui/joy'
import Box from '@mui/joy/Box'
import Link from '@mui/joy/Link'
import Typography from '@mui/joy/Typography'
import * as React from 'react'
function Footer() {
return (
<Card
data-aos-landing-footer
data-aos-delay={200}
data-aos-anchor='[data-aos-landing-footer]'
data-aos='zoom-in-up'
>
<Grid
container
component='footer'
sx={{
display: 'flex',
justifyContent: 'space-around',
p: 4,
// borderTop: '1px solid',
bottom: 0,
width: '100%',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<div className='logo'>
<img src={LogoSVG} alt='logo' width='64px' height='64px' />
</div>
<Box className='flex items-center gap-2'>
<Typography
level='title-lg'
sx={{
fontWeight: 700,
fontSize: 24,
}}
>
Done
<span
style={{
color: '#06b6d4',
fontWeight: 600,
}}
>
tick
</span>
</Typography>
<span
style={{
fontSize: 12,
fontWeight: 700,
position: 'relative',
top: 12,
right: 45,
}}
>
Beta
</span>
</Box>
</Box>
<Box>
<Typography level='body2' fontWeight='bold' mb={1}>
Github
</Typography>
<Link
href='https://github.com/donetick/core'
level='body2'
sx={{ display: 'block' }}
>
Core(Backend)
</Link>
<Link
href='https://github.com/donetick/frontend'
level='body2'
sx={{ display: 'block' }}
>
Frontend
</Link>
<Link
href='https://github.com/donetick/hassio-addons'
level='body2'
sx={{ display: 'block' }}
>
Home Assistant Addon
</Link>
<Link
href='https://github.com/orgs/Donetick/packages'
level='body2'
sx={{ display: 'block' }}
>
Packages
</Link>
</Box>
<Box>
<Typography level='body2' fontWeight='bold' mb={1}>
Links
</Typography>
<Link disabled={true} level='body2' sx={{ display: 'block' }}>
Roadmap(soon)
</Link>
<Link disabled={true} level='body2' sx={{ display: 'block' }}>
Documentation(soon)
</Link>
<Link disabled={true} level='body2' sx={{ display: 'block' }}>
Changelog(soon)
</Link>
</Box>
{/* <Box>
<Typography level='body2' fontWeight='bold' mb={1}>
Others
</Typography>
<Link href='#' level='body2' sx={{ display: 'block' }}>
Telegram Integration
</Link>
<Link href='#' level='body2' sx={{ display: 'block' }}>
Slash Commands
</Link>
</Box> */}
</Grid>
</Card>
)
}
export default Footer

View file

@ -1,4 +1,4 @@
import { Container, Grid } from '@mui/joy' import { Box, Container, Grid } from '@mui/joy'
import AOS from 'aos' import AOS from 'aos'
import 'aos/dist/aos.css' import 'aos/dist/aos.css'
import { useEffect } from 'react' import { useEffect } from 'react'
@ -8,6 +8,7 @@ import DemoHistory from './DemoHistory'
import DemoMyChore from './DemoMyChore' import DemoMyChore from './DemoMyChore'
import DemoScheduler from './DemoScheduler' import DemoScheduler from './DemoScheduler'
import FeaturesSection from './FeaturesSection' import FeaturesSection from './FeaturesSection'
import Footer from './Footer'
import HomeHero from './HomeHero' import HomeHero from './HomeHero'
const Landing = () => { const Landing = () => {
const Navigate = useNavigate() const Navigate = useNavigate()
@ -39,6 +40,17 @@ const Landing = () => {
</Grid> </Grid>
<FeaturesSection /> <FeaturesSection />
{/* <PricingSection /> */} {/* <PricingSection /> */}
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
mt: 5,
mb: 5,
}}
></Box>
<Footer />
</Container> </Container>
) )
} }

View file

@ -0,0 +1,51 @@
import { Box, CircularProgress, Container } from '@mui/joy'
import { Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import Logo from '../../Logo'
const LoadingComponent = () => {
const [message, setMessage] = useState('Loading...')
const [subMessage, setSubMessage] = useState('')
useEffect(() => {
// if loading took more than 5 seconds update submessage to mention there might be an error:
const timeout = setTimeout(() => {
setSubMessage(
'This is taking longer than usual. There might be an issue.',
)
}, 5000)
return () => clearTimeout(timeout)
}, [])
return (
<Container className='flex h-full items-center justify-center'>
<Box
className='flex flex-col items-center justify-center'
sx={{
minHeight: '80vh',
}}
>
<CircularProgress
color='success'
sx={{ '--CircularProgress-size': '200px' }}
>
<Logo />
</CircularProgress>
<Box
className='flex items-center gap-2'
sx={{
fontWeight: 700,
fontSize: 24,
mt: 2,
}}
>
{message}
</Box>
<Typography level='h2' fontWeight={500} textAlign={'center'}>
{subMessage}
</Typography>
</Box>
</Container>
)
}
export default LoadingComponent

View file

@ -21,7 +21,7 @@ import {
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { useState } from 'react' import { useState } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { version } from '../../../package.json' import { version } from '../../../package.json'
import NavBarLink from './NavBarLink' import NavBarLink from './NavBarLink'
const links = [ const links = [
@ -63,6 +63,7 @@ const links = [
] ]
const NavBar = () => { const NavBar = () => {
const navigate = useNavigate()
const [drawerOpen, setDrawerOpen] = useState(false) const [drawerOpen, setDrawerOpen] = useState(false)
const [openDrawer, closeDrawer] = [ const [openDrawer, closeDrawer] = [
() => setDrawerOpen(true), () => setDrawerOpen(true),
@ -89,7 +90,12 @@ const NavBar = () => {
<IconButton size='sm' variant='plain' onClick={() => setDrawerOpen(true)}> <IconButton size='sm' variant='plain' onClick={() => setDrawerOpen(true)}>
<MenuRounded /> <MenuRounded />
</IconButton> </IconButton>
<Box className='flex items-center gap-2'> <Box
className='flex items-center gap-2'
onClick={() => {
navigate('/my/chores')
}}
>
<img component='img' src={Logo} width='34' /> <img component='img' src={Logo} width='34' />
<Typography <Typography
level='title-lg' level='title-lg'