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_IS_LANDING_DEFAULT=false
|
|
@ -20,6 +20,12 @@ import TermsView from '../views/Terms/TermsView'
|
|||
import TestView from '../views/TestView/Test'
|
||||
import ThingsHistory from '../views/Things/ThingsHistory'
|
||||
import ThingsView from '../views/Things/ThingsView'
|
||||
const getMainRoute = () => {
|
||||
if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') {
|
||||
return <Landing />
|
||||
}
|
||||
return <MyChores />
|
||||
}
|
||||
const Router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
|
@ -28,7 +34,7 @@ const Router = createBrowserRouter([
|
|||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <Landing />,
|
||||
element: getMainRoute(),
|
||||
},
|
||||
{
|
||||
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 TextModal from '../Modals/Inputs/TextModal'
|
||||
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 [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
|
||||
React.useState(false)
|
||||
|
@ -367,6 +374,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
|
|||
</Chip>
|
||||
|
||||
<Card
|
||||
style={viewOnly ? { pointerEvents: 'none' } : {}}
|
||||
variant='plain'
|
||||
sx={{
|
||||
...sx,
|
||||
|
|
|
@ -3,12 +3,10 @@ import {
|
|||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Grid,
|
||||
List,
|
||||
ListDivider,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemDecorator,
|
||||
|
@ -21,6 +19,7 @@ import { Link, useParams } from 'react-router-dom'
|
|||
import { API_URL } from '../../Config'
|
||||
import { GetAllCircleMembers } from '../../utils/Fetcher'
|
||||
import { Fetch } from '../../utils/TokenManager'
|
||||
import HistoryCard from './HistoryCard'
|
||||
|
||||
const ChoreHistory = () => {
|
||||
const [choreHistory, setChoresHistory] = useState([])
|
||||
|
@ -144,25 +143,6 @@ const ChoreHistory = () => {
|
|||
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) {
|
||||
return <CircularProgress /> // Show loading indicator
|
||||
}
|
||||
|
@ -251,89 +231,14 @@ const ChoreHistory = () => {
|
|||
{/* Chore History List (Updated Style) */}
|
||||
|
||||
<List sx={{ p: 0 }}>
|
||||
{choreHistory.map((chore, index) => (
|
||||
<>
|
||||
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
|
||||
{' '}
|
||||
{/* Adjusted spacing and alignment */}
|
||||
<ListItemDecorator>
|
||||
<Avatar sx={{ mr: 1 }}>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{choreHistory.map((historyEntry, index) => (
|
||||
<HistoryCard
|
||||
historyEntry={historyEntry}
|
||||
performers={performers}
|
||||
allHistory={choreHistory}
|
||||
key={index}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</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 Box from '@mui/material/Box'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import Logo from '@/assets/logo.svg'
|
||||
import screenShotMyChore from '@/assets/screenshot-my-chore.png'
|
||||
import { GitHub } from '@mui/icons-material'
|
||||
import useWindowWidth from '../../hooks/useWindowWidth'
|
||||
|
||||
const HomeHero = () => {
|
||||
const navigate = useNavigate()
|
||||
const windowWidth = useWindowWidth()
|
||||
const windowThreshold = 600
|
||||
const HERO_TEXT_THAT = [
|
||||
// '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.',
|
||||
|
@ -21,7 +24,7 @@ const HomeHero = () => {
|
|||
|
||||
const [heroTextIndex, setHeroTextIndex] = React.useState(0)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
// const intervalId = setInterval(
|
||||
// () => setHeroTextIndex(index => index + 1),
|
||||
// 4000, // every 4 seconds
|
||||
|
@ -58,6 +61,17 @@ const HomeHero = () => {
|
|||
>
|
||||
tick
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
position: 'relative',
|
||||
top: 12,
|
||||
right: 45,
|
||||
}}
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
|
@ -162,23 +176,24 @@ const HomeHero = () => {
|
|||
</Box>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={5}>
|
||||
<div className='flex justify-center'>
|
||||
<img
|
||||
src={screenShotMyChore}
|
||||
width={'100%'}
|
||||
style={{
|
||||
maxWidth: 300,
|
||||
}}
|
||||
height={'auto'}
|
||||
alt='Hero img'
|
||||
data-aos-delay={100 * 2}
|
||||
data-aos-anchor='[data-aos-id-hero]'
|
||||
data-aos='fade-left'
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
{windowWidth > windowThreshold && (
|
||||
<Grid item xs={12} md={5}>
|
||||
<div className='flex justify-center'>
|
||||
<img
|
||||
src={screenShotMyChore}
|
||||
width={'100%'}
|
||||
style={{
|
||||
maxWidth: 300,
|
||||
}}
|
||||
height={'auto'}
|
||||
alt='Hero img'
|
||||
data-aos-delay={100 * 2}
|
||||
data-aos-anchor='[data-aos-id-hero]'
|
||||
data-aos='fade-left'
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
import { Container } from '@mui/joy'
|
||||
import { Container, Grid } from '@mui/joy'
|
||||
import AOS from 'aos'
|
||||
import 'aos/dist/aos.css'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
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 HomeHero from './HomeHero'
|
||||
const Landing = () => {
|
||||
const Navigate = useNavigate()
|
||||
const getCurrentUser = () => {
|
||||
return JSON.parse(localStorage.getItem('user'))
|
||||
}
|
||||
const [users, setUsers] = useState([])
|
||||
const [currentUser, setCurrentUser] = useState(getCurrentUser())
|
||||
|
||||
useEffect(() => {
|
||||
AOS.init({
|
||||
once: false, // whether animation should happen only once - while scrolling down
|
||||
|
@ -22,6 +20,23 @@ const Landing = () => {
|
|||
return (
|
||||
<Container className='flex h-full items-center justify-center'>
|
||||
<HomeHero />
|
||||
<Grid
|
||||
overflow={'hidden'}
|
||||
container
|
||||
spacing={4}
|
||||
sx={{
|
||||
mt: 5,
|
||||
mb: 5,
|
||||
// align item vertically:
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<DemoMyChore />
|
||||
<DemoAssignee />
|
||||
<DemoScheduler />
|
||||
|
||||
<DemoHistory />
|
||||
</Grid>
|
||||
<FeaturesSection />
|
||||
{/* <PricingSection /> */}
|
||||
</Container>
|
||||
|
|
|
@ -71,12 +71,18 @@ const NavBar = () => {
|
|||
const location = useLocation()
|
||||
// if url has /landing then remove the navbar:
|
||||
if (
|
||||
['/', '/signup', '/login', '/landing', '/forgot-password'].includes(
|
||||
['/signup', '/login', '/landing', '/forgot-password'].includes(
|
||||
location.pathname,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
location.pathname === '/' &&
|
||||
import.meta.env.VITE_IS_LANDING_DEFAULT === 'true'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className='flex gap-2 p-3'>
|
||||
|
@ -102,6 +108,17 @@ const NavBar = () => {
|
|||
tick✓
|
||||
</span>
|
||||
</Typography>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
position: 'relative',
|
||||
top: 12,
|
||||
right: 45,
|
||||
}}
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
</Box>
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
|
|
Loading…
Add table
Reference in a new issue