Merge branch 'dev'
This commit is contained in:
commit
c684bdb9ec
13 changed files with 729 additions and 135 deletions
1
.env
1
.env
|
@ -1 +1,2 @@
|
||||||
VITE_APP_API_URL=http://localhost:2021
|
VITE_APP_API_URL=http://localhost:2021
|
||||||
|
VITE_IS_LANDING_DEFAULT=false
|
|
@ -20,6 +20,12 @@ import TermsView from '../views/Terms/TermsView'
|
||||||
import TestView from '../views/TestView/Test'
|
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'
|
||||||
|
const getMainRoute = () => {
|
||||||
|
if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') {
|
||||||
|
return <Landing />
|
||||||
|
}
|
||||||
|
return <MyChores />
|
||||||
|
}
|
||||||
const Router = createBrowserRouter([
|
const Router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
@ -28,7 +34,7 @@ const Router = createBrowserRouter([
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <Landing />,
|
element: getMainRoute(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
|
|
14
src/hooks/useWindowWidth.js
Normal file
14
src/hooks/useWindowWidth.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
function useWindowWidth() {
|
||||||
|
const [width, setWidth] = useState(window.innerWidth)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => setWidth(window.innerWidth)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useWindowWidth
|
|
@ -45,7 +45,14 @@ import DateModal from '../Modals/Inputs/DateModal'
|
||||||
import SelectModal from '../Modals/Inputs/SelectModal'
|
import SelectModal from '../Modals/Inputs/SelectModal'
|
||||||
import TextModal from '../Modals/Inputs/TextModal'
|
import TextModal from '../Modals/Inputs/TextModal'
|
||||||
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
|
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
|
||||||
const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
|
const ChoreCard = ({
|
||||||
|
chore,
|
||||||
|
performers,
|
||||||
|
onChoreUpdate,
|
||||||
|
onChoreRemove,
|
||||||
|
sx,
|
||||||
|
viewOnly,
|
||||||
|
}) => {
|
||||||
const [activeUserId, setActiveUserId] = React.useState(0)
|
const [activeUserId, setActiveUserId] = React.useState(0)
|
||||||
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
|
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
|
||||||
React.useState(false)
|
React.useState(false)
|
||||||
|
@ -367,6 +374,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
|
style={viewOnly ? { pointerEvents: 'none' } : {}}
|
||||||
variant='plain'
|
variant='plain'
|
||||||
sx={{
|
sx={{
|
||||||
...sx,
|
...sx,
|
||||||
|
|
|
@ -3,12 +3,10 @@ import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Chip,
|
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
Grid,
|
Grid,
|
||||||
List,
|
List,
|
||||||
ListDivider,
|
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemContent,
|
ListItemContent,
|
||||||
ListItemDecorator,
|
ListItemDecorator,
|
||||||
|
@ -21,6 +19,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 HistoryCard from './HistoryCard'
|
||||||
|
|
||||||
const ChoreHistory = () => {
|
const ChoreHistory = () => {
|
||||||
const [choreHistory, setChoresHistory] = useState([])
|
const [choreHistory, setChoresHistory] = useState([])
|
||||||
|
@ -144,25 +143,6 @@ const ChoreHistory = () => {
|
||||||
setHistoryInfo(historyInfo)
|
setHistoryInfo(historyInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeDifference(startDate, endDate) {
|
|
||||||
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
|
|
||||||
let timeValue = diffInMinutes
|
|
||||||
let unit = 'minute'
|
|
||||||
|
|
||||||
if (diffInMinutes >= 60) {
|
|
||||||
const diffInHours = moment(startDate).diff(endDate, 'hours')
|
|
||||||
timeValue = diffInHours
|
|
||||||
unit = 'hour'
|
|
||||||
|
|
||||||
if (diffInHours >= 24) {
|
|
||||||
const diffInDays = moment(startDate).diff(endDate, 'days')
|
|
||||||
timeValue = diffInDays
|
|
||||||
unit = 'day'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
|
|
||||||
}
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <CircularProgress /> // Show loading indicator
|
return <CircularProgress /> // Show loading indicator
|
||||||
}
|
}
|
||||||
|
@ -251,89 +231,14 @@ const ChoreHistory = () => {
|
||||||
{/* Chore History List (Updated Style) */}
|
{/* Chore History List (Updated Style) */}
|
||||||
|
|
||||||
<List sx={{ p: 0 }}>
|
<List sx={{ p: 0 }}>
|
||||||
{choreHistory.map((chore, index) => (
|
{choreHistory.map((historyEntry, index) => (
|
||||||
<>
|
<HistoryCard
|
||||||
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
|
historyEntry={historyEntry}
|
||||||
{' '}
|
performers={performers}
|
||||||
{/* Adjusted spacing and alignment */}
|
allHistory={choreHistory}
|
||||||
<ListItemDecorator>
|
key={index}
|
||||||
<Avatar sx={{ mr: 1 }}>
|
index={index}
|
||||||
{performers
|
/>
|
||||||
.find(p => p.userId === chore.completedBy)
|
|
||||||
?.displayName?.charAt(0) || '?'}
|
|
||||||
</Avatar>
|
|
||||||
</ListItemDecorator>
|
|
||||||
<ListItemContent sx={{ my: 0 }}>
|
|
||||||
{' '}
|
|
||||||
{/* Removed vertical margin */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography level='body1' sx={{ fontWeight: 'md' }}>
|
|
||||||
{moment(chore.completedAt).format('ddd MM/DD/yyyy HH:mm')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Chip>
|
|
||||||
{chore.dueDate && chore.completedAt > chore.dueDate
|
|
||||||
? 'Late'
|
|
||||||
: 'On Time'}
|
|
||||||
</Chip>
|
|
||||||
</Box>
|
|
||||||
<Typography level='body2' color='text.tertiary'>
|
|
||||||
<Chip>
|
|
||||||
{
|
|
||||||
performers.find(p => p.userId === chore.completedBy)
|
|
||||||
?.displayName
|
|
||||||
}
|
|
||||||
</Chip>{' '}
|
|
||||||
completed
|
|
||||||
{chore.completedBy !== chore.assignedTo && (
|
|
||||||
<>
|
|
||||||
{', '}
|
|
||||||
assigned to{' '}
|
|
||||||
<Chip>
|
|
||||||
{
|
|
||||||
performers.find(p => p.userId === chore.assignedTo)
|
|
||||||
?.displayName
|
|
||||||
}
|
|
||||||
</Chip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
{chore.dueDate && (
|
|
||||||
<Typography level='body2' color='text.tertiary'>
|
|
||||||
Due: {moment(chore.dueDate).format('ddd MM/DD/yyyy')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{chore.notes && (
|
|
||||||
<Typography level='body2' color='text.tertiary'>
|
|
||||||
Note: {chore.notes}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</ListItemContent>
|
|
||||||
</ListItem>
|
|
||||||
{index < choreHistory.length - 1 && (
|
|
||||||
<>
|
|
||||||
<ListDivider component='li'>
|
|
||||||
{/* time between two completion: */}
|
|
||||||
{index < choreHistory.length - 1 &&
|
|
||||||
choreHistory[index + 1].completedAt && (
|
|
||||||
<Typography level='body3' color='text.tertiary'>
|
|
||||||
{formatTimeDifference(
|
|
||||||
chore.completedAt,
|
|
||||||
choreHistory[index + 1].completedAt,
|
|
||||||
)}{' '}
|
|
||||||
before
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</ListDivider>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
120
src/views/History/HistoryCard.jsx
Normal file
120
src/views/History/HistoryCard.jsx
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Chip,
|
||||||
|
ListDivider,
|
||||||
|
ListItem,
|
||||||
|
ListItemContent,
|
||||||
|
ListItemDecorator,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
const HistoryCard = ({ allHistory, performers, historyEntry, index }) => {
|
||||||
|
function formatTimeDifference(startDate, endDate) {
|
||||||
|
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
|
||||||
|
let timeValue = diffInMinutes
|
||||||
|
let unit = 'minute'
|
||||||
|
|
||||||
|
if (diffInMinutes >= 60) {
|
||||||
|
const diffInHours = moment(startDate).diff(endDate, 'hours')
|
||||||
|
timeValue = diffInHours
|
||||||
|
unit = 'hour'
|
||||||
|
|
||||||
|
if (diffInHours >= 24) {
|
||||||
|
const diffInDays = moment(startDate).diff(endDate, 'days')
|
||||||
|
timeValue = diffInDays
|
||||||
|
unit = 'day'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
|
||||||
|
{' '}
|
||||||
|
{/* Adjusted spacing and alignment */}
|
||||||
|
<ListItemDecorator>
|
||||||
|
<Avatar sx={{ mr: 1 }}>
|
||||||
|
{performers
|
||||||
|
.find(p => p.userId === historyEntry.completedBy)
|
||||||
|
?.displayName?.charAt(0) || '?'}
|
||||||
|
</Avatar>
|
||||||
|
</ListItemDecorator>
|
||||||
|
<ListItemContent sx={{ my: 0 }}>
|
||||||
|
{' '}
|
||||||
|
{/* Removed vertical margin */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level='body1' sx={{ fontWeight: 'md' }}>
|
||||||
|
{moment(historyEntry.completedAt).format('ddd MM/DD/yyyy HH:mm')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Chip>
|
||||||
|
{historyEntry.dueDate &&
|
||||||
|
historyEntry.completedAt > historyEntry.dueDate
|
||||||
|
? 'Late'
|
||||||
|
: 'On Time'}
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
<Typography level='body2' color='text.tertiary'>
|
||||||
|
<Chip>
|
||||||
|
{
|
||||||
|
performers.find(p => p.userId === historyEntry.completedBy)
|
||||||
|
?.displayName
|
||||||
|
}
|
||||||
|
</Chip>{' '}
|
||||||
|
completed
|
||||||
|
{historyEntry.completedBy !== historyEntry.assignedTo && (
|
||||||
|
<>
|
||||||
|
{', '}
|
||||||
|
assigned to{' '}
|
||||||
|
<Chip>
|
||||||
|
{
|
||||||
|
performers.find(p => p.userId === historyEntry.assignedTo)
|
||||||
|
?.displayName
|
||||||
|
}
|
||||||
|
</Chip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
{historyEntry.dueDate && (
|
||||||
|
<Typography level='body2' color='text.tertiary'>
|
||||||
|
Due: {moment(historyEntry.dueDate).format('ddd MM/DD/yyyy')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{historyEntry.notes && (
|
||||||
|
<Typography level='body2' color='text.tertiary'>
|
||||||
|
Note: {historyEntry.notes}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</ListItemContent>
|
||||||
|
</ListItem>
|
||||||
|
{index < allHistory.length - 1 && (
|
||||||
|
<>
|
||||||
|
<ListDivider component='li'>
|
||||||
|
{/* time between two completion: */}
|
||||||
|
{index < allHistory.length - 1 &&
|
||||||
|
allHistory[index + 1].completedAt && (
|
||||||
|
<Typography level='body3' color='text.tertiary'>
|
||||||
|
{formatTimeDifference(
|
||||||
|
historyEntry.completedAt,
|
||||||
|
allHistory[index + 1].completedAt,
|
||||||
|
)}{' '}
|
||||||
|
before
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</ListDivider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryCard
|
192
src/views/Landing/DemoAssignee.jsx
Normal file
192
src/views/Landing/DemoAssignee.jsx
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Grid,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import { useState } from 'react'
|
||||||
|
const ASSIGN_STRATEGIES = [
|
||||||
|
'random',
|
||||||
|
'least_assigned',
|
||||||
|
'least_completed',
|
||||||
|
'keep_last_assigned',
|
||||||
|
]
|
||||||
|
const DemoAssignee = () => {
|
||||||
|
const [assignStrategy, setAssignStrategy] = useState('random')
|
||||||
|
const [assignees, setAssignees] = useState([
|
||||||
|
{
|
||||||
|
userId: 3,
|
||||||
|
id: 3,
|
||||||
|
displayName: 'Ryan',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const [assignedTo, setAssignedTo] = useState(3)
|
||||||
|
const performers = [
|
||||||
|
{
|
||||||
|
userId: 1,
|
||||||
|
id: 1,
|
||||||
|
displayName: 'Mo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: 2,
|
||||||
|
id: 2,
|
||||||
|
displayName: 'Jiji',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: 3,
|
||||||
|
id: 3,
|
||||||
|
displayName: 'Ryan',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={6} data-aos-create-chore-assignee>
|
||||||
|
<Box
|
||||||
|
mt={2}
|
||||||
|
data-aos-delay={200}
|
||||||
|
data-aos-anchor='[data-aos-create-chore-assignee]'
|
||||||
|
data-aos='fade-right'
|
||||||
|
>
|
||||||
|
<Typography level='h4'>Assignees :</Typography>
|
||||||
|
<Typography level='h5'>Who can do this chore?</Typography>
|
||||||
|
<Card>
|
||||||
|
<List
|
||||||
|
orientation='horizontal'
|
||||||
|
wrap
|
||||||
|
sx={{
|
||||||
|
'--List-gap': '8px',
|
||||||
|
'--ListItem-radius': '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{performers?.map(item => (
|
||||||
|
<ListItem key={item.id}>
|
||||||
|
<Checkbox
|
||||||
|
// disabled={index === 0}
|
||||||
|
checked={assignees.find(a => a.userId == item.id) != null}
|
||||||
|
onClick={() => {
|
||||||
|
if (assignees.find(a => a.userId == item.id)) {
|
||||||
|
setAssignees(
|
||||||
|
assignees.filter(i => i.userId !== item.id),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setAssignees([...assignees, { userId: item.id }])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
overlay
|
||||||
|
disableIcon
|
||||||
|
variant='soft'
|
||||||
|
label={item.displayName}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
mt={2}
|
||||||
|
data-aos-delay={300}
|
||||||
|
data-aos-anchor='[data-aos-create-chore-assignee]'
|
||||||
|
data-aos='fade-right'
|
||||||
|
>
|
||||||
|
<Typography level='h4'>Assigned :</Typography>
|
||||||
|
<Typography level='h5'>
|
||||||
|
Who is assigned the next due chore?
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder={
|
||||||
|
assignees.length === 0
|
||||||
|
? 'No Assignees yet can perform this chore'
|
||||||
|
: 'Select an assignee for this chore'
|
||||||
|
}
|
||||||
|
disabled={assignees.length === 0}
|
||||||
|
value={assignedTo > -1 ? assignedTo : null}
|
||||||
|
>
|
||||||
|
{performers
|
||||||
|
?.filter(p => assignees.find(a => a.userId == p.userId))
|
||||||
|
.map((item, index) => (
|
||||||
|
<Option
|
||||||
|
value={item.id}
|
||||||
|
key={item.displayName}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
{item.displayName}
|
||||||
|
{/* <Chip size='sm' color='neutral' variant='soft'>
|
||||||
|
</Chip> */}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
mt={2}
|
||||||
|
data-aos-delay={400}
|
||||||
|
data-aos-anchor='[data-aos-create-chore-assignee]'
|
||||||
|
data-aos='fade-right'
|
||||||
|
>
|
||||||
|
<Typography level='h4'>Picking Mode :</Typography>
|
||||||
|
<Typography level='h5'>
|
||||||
|
How to pick the next assignee for the following chore?
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<List
|
||||||
|
orientation='horizontal'
|
||||||
|
wrap
|
||||||
|
sx={{
|
||||||
|
'--List-gap': '8px',
|
||||||
|
'--ListItem-radius': '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ASSIGN_STRATEGIES.map((item, idx) => (
|
||||||
|
<ListItem key={item}>
|
||||||
|
<Checkbox
|
||||||
|
// disabled={index === 0}
|
||||||
|
checked={assignStrategy === item}
|
||||||
|
onClick={() => setAssignStrategy(item)}
|
||||||
|
overlay
|
||||||
|
disableIcon
|
||||||
|
variant='soft'
|
||||||
|
label={item
|
||||||
|
.split('_')
|
||||||
|
.map(x => x.charAt(0).toUpperCase() + x.slice(1))
|
||||||
|
.join(' ')}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6} data-aos-create-chore-section-assignee>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
py: 6,
|
||||||
|
}}
|
||||||
|
data-aos-delay={200}
|
||||||
|
data-aos-anchor='[data-aos-create-chore-section-assignee]'
|
||||||
|
data-aos='fade-left'
|
||||||
|
>
|
||||||
|
<Typography level='h3' textAlign='center' sx={{ mt: 2, mb: 4 }}>
|
||||||
|
Flexible Task Assignment
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-lg' textAlign='center' sx={{ mb: 4 }}>
|
||||||
|
Whether you’re a solo user managing personal tasks or coordinating
|
||||||
|
chores with others, Donetick provides robust assignment options.
|
||||||
|
Assign tasks to different people and choose specific rotation
|
||||||
|
strategies, such as assigning tasks based on who completed the most
|
||||||
|
or least, randomly rotating assignments, or sticking with the last
|
||||||
|
assigned person.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DemoAssignee
|
95
src/views/Landing/DemoHistory.jsx
Normal file
95
src/views/Landing/DemoHistory.jsx
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { Box, Card, Grid, List, Typography } from '@mui/joy'
|
||||||
|
import moment from 'moment'
|
||||||
|
import HistoryCard from '../History/HistoryCard'
|
||||||
|
|
||||||
|
const DemoHistory = () => {
|
||||||
|
const allHistory = [
|
||||||
|
{
|
||||||
|
id: 32,
|
||||||
|
choreId: 12,
|
||||||
|
completedAt: moment().format(),
|
||||||
|
completedBy: 1,
|
||||||
|
assignedTo: 1,
|
||||||
|
notes: null,
|
||||||
|
dueDate: moment().format(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 31,
|
||||||
|
choreId: 12,
|
||||||
|
completedAt: moment().day(-1).format(),
|
||||||
|
completedBy: 1,
|
||||||
|
assignedTo: 1,
|
||||||
|
notes: 'Need to be replaced with a new one',
|
||||||
|
dueDate: moment().day(-2).format(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 31,
|
||||||
|
choreId: 12,
|
||||||
|
completedAt: moment().day(-10).format(),
|
||||||
|
completedBy: 1,
|
||||||
|
assignedTo: 1,
|
||||||
|
notes: null,
|
||||||
|
dueDate: moment().day(-10).format(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const performers = [
|
||||||
|
{
|
||||||
|
userId: 1,
|
||||||
|
displayName: 'Ryan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: 2,
|
||||||
|
displayName: 'Sarah',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={6} data-aos-history-list>
|
||||||
|
<Box sx={{ borderRadius: 'sm', p: 2, boxShadow: 'md' }}>
|
||||||
|
<List sx={{ p: 0 }}>
|
||||||
|
{allHistory.map((historyEntry, index) => (
|
||||||
|
<div
|
||||||
|
data-aos-delay={100 * index + 200}
|
||||||
|
data-aos-anchor='[data-aos-history-list]'
|
||||||
|
data-aos='fade-right'
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<HistoryCard
|
||||||
|
allHistory={allHistory}
|
||||||
|
historyEntry={historyEntry}
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
performers={performers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6} data-aos-history-demo-section>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
py: 6,
|
||||||
|
}}
|
||||||
|
data-aos-delay={200}
|
||||||
|
data-aos-anchor='[data-aos-history-demo-section]'
|
||||||
|
data-aos='fade-left'
|
||||||
|
>
|
||||||
|
<Typography level='h3' textAlign='center' sx={{ mt: 2, mb: 4 }}>
|
||||||
|
History with a purpose
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-lg' textAlign='center' sx={{ mb: 4 }}>
|
||||||
|
Keep track of all your chores and tasks with ease. Donetick records
|
||||||
|
due dates, completion dates, and who completed each task. Any notes
|
||||||
|
added to tasks are also tracked, providing a complete history for
|
||||||
|
your reference. Stay organized and informed with detailed task
|
||||||
|
tracking.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default DemoHistory
|
138
src/views/Landing/DemoMyChore.jsx
Normal file
138
src/views/Landing/DemoMyChore.jsx
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
import { Card, Grid, Typography } from '@mui/joy'
|
||||||
|
import moment from 'moment'
|
||||||
|
import ChoreCard from '../Chores/ChoreCard'
|
||||||
|
|
||||||
|
const DemoMyChore = () => {
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: '♻️ Take out recycle ',
|
||||||
|
frequencyType: 'days_of_the_week',
|
||||||
|
frequency: 1,
|
||||||
|
frequencyMetadata:
|
||||||
|
'{"days":["thursday"],"time":"2024-07-07T22:00:00-04:00"}',
|
||||||
|
nextDueDate: moment().add(1, 'days').hour(8).minute(0).toISOString(),
|
||||||
|
isRolling: false,
|
||||||
|
assignedTo: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
name: '🐜 Spray Pesticide',
|
||||||
|
frequencyType: 'interval',
|
||||||
|
frequency: 3,
|
||||||
|
frequencyMetadata: '{"unit":"months"}',
|
||||||
|
nextDueDate: moment().subtract(7, 'day').toISOString(),
|
||||||
|
isRolling: false,
|
||||||
|
assignedTo: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: '🍂 Gutter Cleaning',
|
||||||
|
frequencyType: 'day_of_the_month',
|
||||||
|
frequency: 1,
|
||||||
|
frequencyMetadata: '{"months":["may"]}',
|
||||||
|
nextDueDate: moment()
|
||||||
|
.month('may')
|
||||||
|
.year(moment().year() + 1)
|
||||||
|
.date(1)
|
||||||
|
.hour(17)
|
||||||
|
.minute(0)
|
||||||
|
.toISOString(),
|
||||||
|
isRolling: false,
|
||||||
|
assignedTo: 1,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: 10,
|
||||||
|
// name: '💨 Air dust Synology NAS and',
|
||||||
|
// frequencyType: 'interval',
|
||||||
|
// frequency: 12,
|
||||||
|
// frequencyMetadata: '{"unit":"weeks"}',
|
||||||
|
// nextDueDate: '2024-07-24T17:18:00Z',
|
||||||
|
// isRolling: false,
|
||||||
|
// assignedTo: 1,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 8,
|
||||||
|
// name: '🛁 Deep Cleaning Bathroom',
|
||||||
|
// frequencyType: 'monthly',
|
||||||
|
// frequency: 1,
|
||||||
|
// frequencyMetadata: '{}',
|
||||||
|
// nextDueDate: '2024-08-04T17:15:00Z',
|
||||||
|
// isRolling: false,
|
||||||
|
// assignedTo: 1,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 11,
|
||||||
|
// name: '☴ Replace AC Air filter',
|
||||||
|
// frequencyType: 'adaptive',
|
||||||
|
// frequency: 1,
|
||||||
|
// frequencyMetadata: '{"unit":"days"}',
|
||||||
|
// nextDueDate: moment().add(120, 'days').toISOString(),
|
||||||
|
// isRolling: false,
|
||||||
|
// assignedTo: 1,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 6,
|
||||||
|
// name: '🍂 Gutter Cleaning ',
|
||||||
|
// frequencyType: 'day_of_the_month',
|
||||||
|
// frequency: 1,
|
||||||
|
// frequencyMetadata: '{"months":["may"]}',
|
||||||
|
// nextDueDate: '2025-05-01T17:00:00Z',
|
||||||
|
// isRolling: false,
|
||||||
|
// assignedTo: 1,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 13,
|
||||||
|
// name: '🚰 Replace Water Filter',
|
||||||
|
// frequencyType: 'yearly',
|
||||||
|
// frequency: 1,
|
||||||
|
// frequencyMetadata: '{}',
|
||||||
|
// nextDueDate: '2025-07-08T01:00:00Z',
|
||||||
|
// isRolling: false,
|
||||||
|
// assignedTo: 1,
|
||||||
|
// },
|
||||||
|
]
|
||||||
|
|
||||||
|
const users = [{ displayName: 'Me', id: 1 }]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={5} data-aos-first-tasks-list>
|
||||||
|
{cards.map((card, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
data-aos-delay={100 * index + 200}
|
||||||
|
data-aos-anchor='[data-aos-first-tasks-list]'
|
||||||
|
data-aos='fade-up'
|
||||||
|
>
|
||||||
|
<ChoreCard chore={card} performers={users} viewOnly={true} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={7} data-aos-my-chore-demo-section>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
py: 6,
|
||||||
|
}}
|
||||||
|
data-aos-delay={200}
|
||||||
|
data-aos-anchor='[data-aos-my-chore-demo-section]'
|
||||||
|
data-aos='fade-left'
|
||||||
|
>
|
||||||
|
<Typography level='h3' textAlign='center' sx={{ mt: 2, mb: 4 }}>
|
||||||
|
Glance at your task and chores
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-lg' textAlign='center' sx={{ mb: 4 }}>
|
||||||
|
Main view prioritize tasks due today, followed by overdue ones, and
|
||||||
|
finally, future tasks or those without due dates. With Donetick, you
|
||||||
|
can view all the tasks you've created (whether assigned to you or
|
||||||
|
not) as well as tasks assigned to you by others. Quickly mark them
|
||||||
|
as done with just one click, ensuring a smooth and efficient task
|
||||||
|
management experience.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DemoMyChore
|
68
src/views/Landing/DemoScheduler.jsx
Normal file
68
src/views/Landing/DemoScheduler.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { Box, Card, Grid, Typography } from '@mui/joy'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import RepeatSection from '../ChoreEdit/RepeatSection'
|
||||||
|
|
||||||
|
const DemoScheduler = () => {
|
||||||
|
const [assignees, setAssignees] = useState([])
|
||||||
|
const [frequency, setFrequency] = useState(2)
|
||||||
|
const [frequencyType, setFrequencyType] = useState('weekly')
|
||||||
|
const [frequencyMetadata, setFrequencyMetadata] = useState({
|
||||||
|
months: ['may', 'june', 'july'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={5} data-aos-create-chore-scheduler>
|
||||||
|
<Box
|
||||||
|
data-aos-delay={300}
|
||||||
|
data-aos-anchor='[data-aos-create-chore-scheduler]'
|
||||||
|
data-aos='fade-right'
|
||||||
|
>
|
||||||
|
<RepeatSection
|
||||||
|
frequency={frequency}
|
||||||
|
onFrequencyUpdate={setFrequency}
|
||||||
|
frequencyType={frequencyType}
|
||||||
|
onFrequencyTypeUpdate={setFrequencyType}
|
||||||
|
frequencyMetadata={frequencyMetadata}
|
||||||
|
onFrequencyMetadataUpdate={setFrequencyMetadata}
|
||||||
|
onFrequencyTimeUpdate={t => {}}
|
||||||
|
frequencyError={null}
|
||||||
|
allUserThings={[]}
|
||||||
|
onTriggerUpdate={thingUpdate => {}}
|
||||||
|
OnTriggerValidate={() => {}}
|
||||||
|
isAttemptToSave={false}
|
||||||
|
selectedThing={null}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={7} data-aos-create-chore-section-scheduler>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
py: 6,
|
||||||
|
}}
|
||||||
|
data-aos-delay={200}
|
||||||
|
data-aos-anchor='[data-aos-create-chore-section-scheduler]'
|
||||||
|
data-aos='fade-left'
|
||||||
|
>
|
||||||
|
<Typography level='h3' textAlign='center' sx={{ mt: 2, mb: 4 }}>
|
||||||
|
Advanced Scheduling and Automation
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-lg' textAlign='center' sx={{ mb: 4 }}>
|
||||||
|
Scheduling is a crucial aspect of managing tasks and chores.
|
||||||
|
Donetick offers basic scheduling options, such as recurring tasks
|
||||||
|
daily, weekly, or yearly, as well as more customizable schedules
|
||||||
|
like specific days of the week or month. For those unsure of exact
|
||||||
|
frequencies, the adaptive scheduling feature averages based on how
|
||||||
|
often you mark a task as completed. Additionally, Donetick supports
|
||||||
|
automation by linking tasks with triggers via API. When specific
|
||||||
|
conditions are met, Donetick’s Things feature will automatically
|
||||||
|
initiate the task, streamlining your workflow.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DemoScheduler
|
|
@ -4,15 +4,18 @@ import { Button } from '@mui/joy'
|
||||||
import Typography from '@mui/joy/Typography'
|
import Typography from '@mui/joy/Typography'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Grid from '@mui/material/Grid'
|
import Grid from '@mui/material/Grid'
|
||||||
import React from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import Logo from '@/assets/logo.svg'
|
import Logo from '@/assets/logo.svg'
|
||||||
import screenShotMyChore from '@/assets/screenshot-my-chore.png'
|
import screenShotMyChore from '@/assets/screenshot-my-chore.png'
|
||||||
import { GitHub } from '@mui/icons-material'
|
import { GitHub } from '@mui/icons-material'
|
||||||
|
import useWindowWidth from '../../hooks/useWindowWidth'
|
||||||
|
|
||||||
const HomeHero = () => {
|
const HomeHero = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const windowWidth = useWindowWidth()
|
||||||
|
const windowThreshold = 600
|
||||||
const HERO_TEXT_THAT = [
|
const HERO_TEXT_THAT = [
|
||||||
// 'Donetick simplifies the entire process, from scheduling and reminders to automatic task assignment and progress tracking.',
|
// 'Donetick simplifies the entire process, from scheduling and reminders to automatic task assignment and progress tracking.',
|
||||||
// 'Donetick is the intuitive task and chore management app designed for groups. Take charge of shared responsibilities, automate your workflow, and achieve more together.',
|
// 'Donetick is the intuitive task and chore management app designed for groups. Take charge of shared responsibilities, automate your workflow, and achieve more together.',
|
||||||
|
@ -21,7 +24,7 @@ const HomeHero = () => {
|
||||||
|
|
||||||
const [heroTextIndex, setHeroTextIndex] = React.useState(0)
|
const [heroTextIndex, setHeroTextIndex] = React.useState(0)
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
// const intervalId = setInterval(
|
// const intervalId = setInterval(
|
||||||
// () => setHeroTextIndex(index => index + 1),
|
// () => setHeroTextIndex(index => index + 1),
|
||||||
// 4000, // every 4 seconds
|
// 4000, // every 4 seconds
|
||||||
|
@ -58,6 +61,17 @@ const HomeHero = () => {
|
||||||
>
|
>
|
||||||
tick
|
tick
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 700,
|
||||||
|
position: 'relative',
|
||||||
|
top: 12,
|
||||||
|
right: 45,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Beta
|
||||||
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
@ -162,7 +176,7 @@ const HomeHero = () => {
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{windowWidth > windowThreshold && (
|
||||||
<Grid item xs={12} md={5}>
|
<Grid item xs={12} md={5}>
|
||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<img
|
<img
|
||||||
|
@ -179,6 +193,7 @@ const HomeHero = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import { Container } from '@mui/joy'
|
import { 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, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import DemoAssignee from './DemoAssignee'
|
||||||
|
import DemoHistory from './DemoHistory'
|
||||||
|
import DemoMyChore from './DemoMyChore'
|
||||||
|
import DemoScheduler from './DemoScheduler'
|
||||||
import FeaturesSection from './FeaturesSection'
|
import FeaturesSection from './FeaturesSection'
|
||||||
import HomeHero from './HomeHero'
|
import HomeHero from './HomeHero'
|
||||||
const Landing = () => {
|
const Landing = () => {
|
||||||
const Navigate = useNavigate()
|
const Navigate = useNavigate()
|
||||||
const getCurrentUser = () => {
|
|
||||||
return JSON.parse(localStorage.getItem('user'))
|
|
||||||
}
|
|
||||||
const [users, setUsers] = useState([])
|
|
||||||
const [currentUser, setCurrentUser] = useState(getCurrentUser())
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AOS.init({
|
AOS.init({
|
||||||
once: false, // whether animation should happen only once - while scrolling down
|
once: false, // whether animation should happen only once - while scrolling down
|
||||||
|
@ -22,6 +20,23 @@ const Landing = () => {
|
||||||
return (
|
return (
|
||||||
<Container className='flex h-full items-center justify-center'>
|
<Container className='flex h-full items-center justify-center'>
|
||||||
<HomeHero />
|
<HomeHero />
|
||||||
|
<Grid
|
||||||
|
overflow={'hidden'}
|
||||||
|
container
|
||||||
|
spacing={4}
|
||||||
|
sx={{
|
||||||
|
mt: 5,
|
||||||
|
mb: 5,
|
||||||
|
// align item vertically:
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DemoMyChore />
|
||||||
|
<DemoAssignee />
|
||||||
|
<DemoScheduler />
|
||||||
|
|
||||||
|
<DemoHistory />
|
||||||
|
</Grid>
|
||||||
<FeaturesSection />
|
<FeaturesSection />
|
||||||
{/* <PricingSection /> */}
|
{/* <PricingSection /> */}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -71,12 +71,18 @@ const NavBar = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
// if url has /landing then remove the navbar:
|
// if url has /landing then remove the navbar:
|
||||||
if (
|
if (
|
||||||
['/', '/signup', '/login', '/landing', '/forgot-password'].includes(
|
['/signup', '/login', '/landing', '/forgot-password'].includes(
|
||||||
location.pathname,
|
location.pathname,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
location.pathname === '/' &&
|
||||||
|
import.meta.env.VITE_IS_LANDING_DEFAULT === 'true'
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='flex gap-2 p-3'>
|
<nav className='flex gap-2 p-3'>
|
||||||
|
@ -102,6 +108,17 @@ const NavBar = () => {
|
||||||
tick✓
|
tick✓
|
||||||
</span>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
position: 'relative',
|
||||||
|
top: 12,
|
||||||
|
right: 45,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Beta
|
||||||
|
</span>
|
||||||
</Box>
|
</Box>
|
||||||
<Drawer
|
<Drawer
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
|
|
Loading…
Add table
Reference in a new issue