From a24134f8525373f5707b6c48aa77d3f4aff70799 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 9 Jul 2024 17:39:16 -0400 Subject: Add useWindowWidth hook and HistoryCard component --- src/hooks/useWindowWidth.js | 19 ++++++ src/views/Chores/ChoreCard.jsx | 10 +++- src/views/History/ChoreHistory.jsx | 113 +++------------------------------- src/views/History/HistoryCard.jsx | 120 +++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 105 deletions(-) create mode 100644 src/hooks/useWindowWidth.js create mode 100644 src/views/History/HistoryCard.jsx diff --git a/src/hooks/useWindowWidth.js b/src/hooks/useWindowWidth.js new file mode 100644 index 0000000..92bf184 --- /dev/null +++ b/src/hooks/useWindowWidth.js @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react' +const useWindowWidth = () => { + const [windowWidth, setWindowWidth] = useState() + + useEffect(() => { + const handleResize = () => { + setWindowWidth(window.innerWidth) + } + + window.addEventListener('resize', handleResize) + + // Cleanup function to remove the event listener + return () => window.removeEventListener('resize', handleResize) + }, []) + + return windowWidth +} + +export default useWindowWidth diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index 5e54e6b..08a5406 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -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 }) => { { 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 // Show loading indicator } @@ -251,89 +231,14 @@ const ChoreHistory = () => { {/* Chore History List (Updated Style) */} - {choreHistory.map((chore, index) => ( - <> - - {' '} - {/* Adjusted spacing and alignment */} - - - {performers - .find(p => p.userId === chore.completedBy) - ?.displayName?.charAt(0) || '?'} - - - - {' '} - {/* Removed vertical margin */} - - - {moment(chore.completedAt).format('ddd MM/DD/yyyy HH:mm')} - - - - {chore.dueDate && chore.completedAt > chore.dueDate - ? 'Late' - : 'On Time'} - - - - - { - performers.find(p => p.userId === chore.completedBy) - ?.displayName - } - {' '} - completed - {chore.completedBy !== chore.assignedTo && ( - <> - {', '} - assigned to{' '} - - { - performers.find(p => p.userId === chore.assignedTo) - ?.displayName - } - - - )} - - {chore.dueDate && ( - - Due: {moment(chore.dueDate).format('ddd MM/DD/yyyy')} - - )} - {chore.notes && ( - - Note: {chore.notes} - - )} - - - {index < choreHistory.length - 1 && ( - <> - - {/* time between two completion: */} - {index < choreHistory.length - 1 && - choreHistory[index + 1].completedAt && ( - - {formatTimeDifference( - chore.completedAt, - choreHistory[index + 1].completedAt, - )}{' '} - before - - )} - - - )} - + {choreHistory.map((historyEntry, index) => ( + ))} diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx new file mode 100644 index 0000000..c606fbf --- /dev/null +++ b/src/views/History/HistoryCard.jsx @@ -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 ( + <> + + {' '} + {/* Adjusted spacing and alignment */} + + + {performers + .find(p => p.userId === historyEntry.completedBy) + ?.displayName?.charAt(0) || '?'} + + + + {' '} + {/* Removed vertical margin */} + + + {moment(historyEntry.completedAt).format('ddd MM/DD/yyyy HH:mm')} + + + + {historyEntry.dueDate && + historyEntry.completedAt > historyEntry.dueDate + ? 'Late' + : 'On Time'} + + + + + { + performers.find(p => p.userId === historyEntry.completedBy) + ?.displayName + } + {' '} + completed + {historyEntry.completedBy !== historyEntry.assignedTo && ( + <> + {', '} + assigned to{' '} + + { + performers.find(p => p.userId === historyEntry.assignedTo) + ?.displayName + } + + + )} + + {historyEntry.dueDate && ( + + Due: {moment(historyEntry.dueDate).format('ddd MM/DD/yyyy')} + + )} + {historyEntry.notes && ( + + Note: {historyEntry.notes} + + )} + + + {index < allHistory.length - 1 && ( + <> + + {/* time between two completion: */} + {index < allHistory.length - 1 && + allHistory[index + 1].completedAt && ( + + {formatTimeDifference( + historyEntry.completedAt, + allHistory[index + 1].completedAt, + )}{' '} + before + + )} + + + )} + + ) +} + +export default HistoryCard -- cgit From 40f1384dfc5ad012b8f47514a07f350141bdf35e Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 9 Jul 2024 17:41:15 -0400 Subject: Add demo scheduler component for advanced scheduling and automation --- src/views/Landing/DemoAssignee.jsx | 192 ++++++++++++++++++++++++++++++++++++ src/views/Landing/DemoHistory.jsx | 95 ++++++++++++++++++ src/views/Landing/DemoMyChore.jsx | 138 ++++++++++++++++++++++++++ src/views/Landing/DemoScheduler.jsx | 68 +++++++++++++ src/views/Landing/HomeHero.jsx | 42 ++++---- src/views/Landing/Landing.jsx | 24 ++++- 6 files changed, 538 insertions(+), 21 deletions(-) create mode 100644 src/views/Landing/DemoAssignee.jsx create mode 100644 src/views/Landing/DemoHistory.jsx create mode 100644 src/views/Landing/DemoMyChore.jsx create mode 100644 src/views/Landing/DemoScheduler.jsx diff --git a/src/views/Landing/DemoAssignee.jsx b/src/views/Landing/DemoAssignee.jsx new file mode 100644 index 0000000..5064cf0 --- /dev/null +++ b/src/views/Landing/DemoAssignee.jsx @@ -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 ( + <> + + + Assignees : + Who can do this chore? + + + {performers?.map(item => ( + + 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} + /> + + ))} + + + + + Assigned : + + Who is assigned the next due chore? + + + + + + Picking Mode : + + How to pick the next assignee for the following chore? + + + + + {ASSIGN_STRATEGIES.map((item, idx) => ( + + setAssignStrategy(item)} + overlay + disableIcon + variant='soft' + label={item + .split('_') + .map(x => x.charAt(0).toUpperCase() + x.slice(1)) + .join(' ')} + /> + + ))} + + + + + + + + Flexible Task Assignment + + + 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. + + + + + ) +} + +export default DemoAssignee diff --git a/src/views/Landing/DemoHistory.jsx b/src/views/Landing/DemoHistory.jsx new file mode 100644 index 0000000..2c713f1 --- /dev/null +++ b/src/views/Landing/DemoHistory.jsx @@ -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 ( + <> + + + + {allHistory.map((historyEntry, index) => ( +
+ +
+ ))} +
+
+
+ + + + History with a purpose + + + 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. + + + + + ) +} +export default DemoHistory diff --git a/src/views/Landing/DemoMyChore.jsx b/src/views/Landing/DemoMyChore.jsx new file mode 100644 index 0000000..be56cae --- /dev/null +++ b/src/views/Landing/DemoMyChore.jsx @@ -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 ( + <> + + {cards.map((card, index) => ( +
+ +
+ ))} +
+ + + + Glance at your task and chores + + + 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. + + + + + ) +} + +export default DemoMyChore diff --git a/src/views/Landing/DemoScheduler.jsx b/src/views/Landing/DemoScheduler.jsx new file mode 100644 index 0000000..5e34adb --- /dev/null +++ b/src/views/Landing/DemoScheduler.jsx @@ -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 ( + <> + + + {}} + frequencyError={null} + allUserThings={[]} + onTriggerUpdate={thingUpdate => {}} + OnTriggerValidate={() => {}} + isAttemptToSave={false} + selectedThing={null} + /> + + + + + + Advanced Scheduling and Automation + + + 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. + + + + + ) +} + +export default DemoScheduler diff --git a/src/views/Landing/HomeHero.jsx b/src/views/Landing/HomeHero.jsx index a96374a..f01d335 100644 --- a/src/views/Landing/HomeHero.jsx +++ b/src/views/Landing/HomeHero.jsx @@ -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 @@ -162,23 +165,24 @@ const HomeHero = () => { - - -
- Hero img -
-
+ {windowWidth > windowThreshold && ( + +
+ Hero img +
+
+ )} ) } diff --git a/src/views/Landing/Landing.jsx b/src/views/Landing/Landing.jsx index 6d3a1ce..fbdefed 100644 --- a/src/views/Landing/Landing.jsx +++ b/src/views/Landing/Landing.jsx @@ -1,8 +1,12 @@ -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 { 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 = () => { @@ -10,7 +14,6 @@ const Landing = () => { const getCurrentUser = () => { return JSON.parse(localStorage.getItem('user')) } - const [users, setUsers] = useState([]) const [currentUser, setCurrentUser] = useState(getCurrentUser()) useEffect(() => { @@ -22,6 +25,23 @@ const Landing = () => { return ( + + + + + + + {/* */} -- cgit From 8da220e990b5ab15509848d4d7a57298eb6e808e Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 9 Jul 2024 17:50:57 -0400 Subject: Refactor useWindowWidth hook to improve readability --- src/hooks/useWindowWidth.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/hooks/useWindowWidth.js b/src/hooks/useWindowWidth.js index 92bf184..fa79cc4 100644 --- a/src/hooks/useWindowWidth.js +++ b/src/hooks/useWindowWidth.js @@ -1,19 +1,14 @@ import { useEffect, useState } from 'react' -const useWindowWidth = () => { - const [windowWidth, setWindowWidth] = useState() +function useWindowWidth() { + const [width, setWidth] = useState(window.innerWidth) useEffect(() => { - const handleResize = () => { - setWindowWidth(window.innerWidth) - } - + const handleResize = () => setWidth(window.innerWidth) window.addEventListener('resize', handleResize) - - // Cleanup function to remove the event listener return () => window.removeEventListener('resize', handleResize) }, []) - return windowWidth + return width } export default useWindowWidth -- cgit From e25a6d3be9b9ae443dd3e1cd57a8c5912cc088b2 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 9 Jul 2024 21:21:00 -0400 Subject: Add VITE_IS_LANDING_DEFAULT flag for conditional rendering of landing page --- .env | 3 ++- src/contexts/RouterContext.jsx | 8 +++++++- src/views/Landing/HomeHero.jsx | 11 +++++++++++ src/views/Landing/Landing.jsx | 7 +------ src/views/components/NavBar.jsx | 19 ++++++++++++++++++- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/.env b/.env index 633f2c7..b7e8b90 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_APP_API_URL=http://localhost:2021 \ No newline at end of file +VITE_APP_API_URL=http://localhost:2021 +VITE_IS_LANDING_DEFAULT=false \ No newline at end of file diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx index 02df35f..4fea83d 100644 --- a/src/contexts/RouterContext.jsx +++ b/src/contexts/RouterContext.jsx @@ -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 + } + return +} const Router = createBrowserRouter([ { path: '/', @@ -28,7 +34,7 @@ const Router = createBrowserRouter([ children: [ { path: '/', - element: , + element: getMainRoute(), }, { path: '/settings', diff --git a/src/views/Landing/HomeHero.jsx b/src/views/Landing/HomeHero.jsx index f01d335..644c9cc 100644 --- a/src/views/Landing/HomeHero.jsx +++ b/src/views/Landing/HomeHero.jsx @@ -61,6 +61,17 @@ const HomeHero = () => { > tick + + Beta + ) diff --git a/src/views/Landing/Landing.jsx b/src/views/Landing/Landing.jsx index fbdefed..a8b650d 100644 --- a/src/views/Landing/Landing.jsx +++ b/src/views/Landing/Landing.jsx @@ -1,7 +1,7 @@ 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' @@ -11,11 +11,6 @@ import FeaturesSection from './FeaturesSection' import HomeHero from './HomeHero' const Landing = () => { const Navigate = useNavigate() - const getCurrentUser = () => { - return JSON.parse(localStorage.getItem('user')) - } - const [currentUser, setCurrentUser] = useState(getCurrentUser()) - useEffect(() => { AOS.init({ once: false, // whether animation should happen only once - while scrolling down diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx index 939c9fd..2da09f5 100644 --- a/src/views/components/NavBar.jsx +++ b/src/views/components/NavBar.jsx @@ -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 (