aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/contexts/RouterContext.jsx5
-rw-r--r--src/utils/Fetcher.jsx15
-rw-r--r--src/views/Authorization/LoginView.jsx11
-rw-r--r--src/views/ChoreEdit/ChoreView.jsx292
-rw-r--r--src/views/Chores/ChoreCard.jsx31
-rw-r--r--src/views/Modals/Inputs/WriteNFCModal.jsx109
6 files changed, 452 insertions, 11 deletions
diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx
index 6077092..02df35f 100644
--- a/src/contexts/RouterContext.jsx
+++ b/src/contexts/RouterContext.jsx
@@ -8,6 +8,7 @@ import ForgotPasswordView from '../views/Authorization/ForgotPasswordView'
import LoginView from '../views/Authorization/LoginView'
import SignupView from '../views/Authorization/Signup'
import UpdatePasswordView from '../views/Authorization/UpdatePasswordView'
+import ChoreView from '../views/ChoreEdit/ChoreView'
import MyChores from '../views/Chores/MyChores'
import JoinCircleView from '../views/Circles/JoinCircle'
import ChoreHistory from '../views/History/ChoreHistory'
@@ -42,6 +43,10 @@ const Router = createBrowserRouter([
element: <ChoreEdit />,
},
{
+ path: '/chores/:choreId',
+ element: <ChoreView />,
+ },
+ {
path: '/chores/create',
element: <ChoreEdit />,
},
diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx
index be3971c..0acc91f 100644
--- a/src/utils/Fetcher.jsx
+++ b/src/utils/Fetcher.jsx
@@ -51,6 +51,19 @@ const GetChoreByID = id => {
headers: HEADERS(),
})
}
+const GetChoreDetailById = id => {
+ return Fetch(`${API_URL}/chores/${id}/details`, {
+ method: 'GET',
+ headers: HEADERS(),
+ })
+}
+
+const MarkChoreComplete = id => {
+ return Fetch(`${API_URL}/chores/${id}/do`, {
+ method: 'POST',
+ headers: HEADERS(),
+ })
+}
const CreateChore = chore => {
return Fetch(`${API_URL}/chores/`, {
method: 'POST',
@@ -238,6 +251,7 @@ export {
GetAllCircleMembers,
GetAllUsers,
GetChoreByID,
+ GetChoreDetailById,
GetChoreHistory,
GetChores,
GetCircleMemberRequests,
@@ -250,6 +264,7 @@ export {
JoinCircle,
LeaveCircle,
login,
+ MarkChoreComplete,
SaveChore,
SaveThing,
signUp,
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index 2ffcef4..721a5b3 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -62,7 +62,6 @@ const LoginView = () => {
}
const loggedWithProvider = function (provider, data) {
- console.log(provider, data)
return fetch(API_URL + `/auth/${provider}/callback`, {
method: 'POST',
headers: {
@@ -80,8 +79,14 @@ const LoginView = () => {
return response.json().then(data => {
localStorage.setItem('ca_token', data.token)
localStorage.setItem('ca_expiration', data.expire)
- // setIsLoggedIn(true);
- getUserProfileAndNavigateToHome()
+
+ const redirectUrl = Cookies.get('ca_redirect')
+ if (redirectUrl) {
+ Cookies.remove('ca_redirect')
+ Navigate(redirectUrl)
+ } else {
+ getUserProfileAndNavigateToHome()
+ }
})
}
return response.json().then(error => {
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
new file mode 100644
index 0000000..8116270
--- /dev/null
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -0,0 +1,292 @@
+import {
+ CalendarMonth,
+ CancelScheduleSend,
+ Check,
+ Checklist,
+ PeopleAlt,
+ Person,
+} from '@mui/icons-material'
+import {
+ Box,
+ Button,
+ Container,
+ Grid,
+ ListItem,
+ ListItemContent,
+ ListItemDecorator,
+ Sheet,
+ Snackbar,
+ styled,
+ Typography,
+} from '@mui/joy'
+import moment from 'moment'
+import { useEffect, useState } from 'react'
+import { useParams, useSearchParams } from 'react-router-dom'
+import {
+ GetAllUsers,
+ GetChoreDetailById,
+ MarkChoreComplete,
+} from '../../utils/Fetcher'
+const IconCard = styled('div')({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#f0f0f0', // Adjust the background color as needed
+ borderRadius: '50%',
+ minWidth: '50px',
+ height: '50px',
+ marginRight: '16px',
+})
+const ChoreView = () => {
+ const [chore, setChore] = useState({})
+
+ const [performers, setPerformers] = useState([])
+ const [infoCards, setInfoCards] = useState([])
+ const { choreId } = useParams()
+
+ // query param `complete=true`
+
+ const [searchParams] = useSearchParams()
+
+ const [isPendingCompletion, setIsPendingCompletion] = useState(false)
+ const [timeoutId, setTimeoutId] = useState(null)
+ const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null)
+ useEffect(() => {
+ Promise.all([
+ GetChoreDetailById(choreId).then(resp => {
+ if (resp.ok) {
+ return resp.json().then(data => {
+ setChore(data.res)
+ })
+ }
+ }),
+ GetAllUsers()
+ .then(response => response.json())
+ .then(data => {
+ setPerformers(data.res)
+ }),
+ ])
+ const auto_complete = searchParams.get('auto_complete')
+ if (auto_complete === 'true') {
+ handleTaskCompletion()
+ }
+ }, [])
+ useEffect(() => {
+ if (chore && performers.length > 0) {
+ generateInfoCards(chore)
+ }
+ }, [chore, performers])
+
+ const generateInfoCards = chore => {
+ const cards = [
+ {
+ icon: <CalendarMonth />,
+ text: 'Due Date',
+ subtext: moment(chore.dueDate).format('MM/DD/YYYY hh:mm A'),
+ },
+ {
+ icon: <PeopleAlt />,
+ text: 'Assigned To',
+ subtext: performers.find(p => p.id === chore.assignedTo)?.displayName,
+ },
+ {
+ icon: <Person />,
+ text: 'Created By',
+ subtext: performers.find(p => p.id === chore.createdBy)?.displayName,
+ },
+ // {
+ // icon: <TextFields />,
+ // text: 'Frequency',
+ // subtext:
+ // chore.frequencyType.charAt(0).toUpperCase() +
+ // chore.frequencyType.slice(1),
+ // },
+ {
+ icon: <Checklist />,
+ text: 'Total Completed',
+ subtext: `${chore.totalCompletedCount}`,
+ },
+ // {
+ // icon: <Timelapse />,
+ // text: 'Last Completed',
+ // subtext:
+ // chore.lastCompletedDate &&
+ // moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A'),
+ // },
+ {
+ icon: <Person />,
+ text: 'Last Completed',
+ subtext: chore.lastCompletedDate
+ ? `${
+ chore.lastCompletedDate &&
+ moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A')
+ }(${
+ performers.find(p => p.id === chore.lastCompletedBy)?.displayName
+ })`
+ : 'Never',
+ },
+ ]
+ setInfoCards(cards)
+ }
+ const handleTaskCompletion = () => {
+ setIsPendingCompletion(true)
+ let seconds = 3 // Starting countdown from 3 seconds
+ setSecondsLeftToCancel(seconds)
+
+ const countdownInterval = setInterval(() => {
+ seconds -= 1
+ setSecondsLeftToCancel(seconds)
+
+ if (seconds <= 0) {
+ clearInterval(countdownInterval) // Stop the countdown when it reaches 0
+ }
+ }, 1000)
+
+ const id = setTimeout(() => {
+ MarkChoreComplete(choreId)
+ .then(resp => {
+ if (resp.ok) {
+ return resp.json().then(data => {
+ setChore(data.res)
+ })
+ }
+ })
+ .then(() => {
+ setIsPendingCompletion(false)
+ clearTimeout(id)
+ clearInterval(countdownInterval) // Ensure to clear this interval as well
+ setTimeoutId(null)
+ setSecondsLeftToCancel(null)
+ })
+ .then(() => {
+ // refetch the chore details
+ GetChoreDetailById(choreId).then(resp => {
+ if (resp.ok) {
+ return resp.json().then(data => {
+ setChore(data.res)
+ })
+ }
+ })
+ })
+ }, 3000)
+
+ setTimeoutId(id)
+ }
+
+ return (
+ <Container maxWidth='sm'>
+ <Sheet
+ variant='plain'
+ sx={{
+ borderRadius: 'sm',
+ p: 2,
+ boxShadow: 'md',
+ minHeight: '90vh',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ }}
+ >
+ <Box>
+ <Typography
+ level='h4'
+ textAlign={'center'}
+ sx={{
+ mt: 2,
+ mb: 4,
+ }}
+ >
+ {chore.name}
+ </Typography>
+
+ <Grid container spacing={1}>
+ {infoCards.map((info, index) => (
+ <Grid key={index} item xs={12} sm={6}>
+ <Sheet
+ sx={{ mb: 1, borderRadius: 'md', p: 1, boxShadow: 'sm' }}
+ >
+ <ListItem>
+ <ListItemDecorator>
+ <IconCard>{info.icon}</IconCard>
+ </ListItemDecorator>
+ <ListItemContent>
+ <Typography level='body1' sx={{ fontWeight: 'md' }}>
+ {info.text}
+ </Typography>
+ <Typography level='body1' color='text.tertiary'>
+ {info.subtext ? info.subtext : '--'}
+ </Typography>
+ </ListItemContent>
+ </ListItem>
+ </Sheet>
+ </Grid>
+ ))}
+ </Grid>
+ </Box>
+ <Box
+ sx={{
+ mt: 6,
+ }}
+ >
+ <Button
+ fullWidth
+ size='lg'
+ sx={{
+ height: 50,
+ mb: 2,
+ }}
+ onClick={handleTaskCompletion}
+ disabled={isPendingCompletion}
+ color={isPendingCompletion ? 'danger' : 'success'}
+ startDecorator={<Check />}
+ >
+ <Box>Mark as done</Box>
+ </Button>
+ {/* <Button
+ sx={{
+ borderRadius: '32px',
+ mt: 1,
+ height: 50,
+ zIndex: 1,
+ }}
+ onClick={() => {
+ Navigate('/my/chores')
+ }}
+ color={isPendingCompletion ? 'danger' : 'success'}
+ startDecorator={isPendingCompletion ? <Close /> : <Check />}
+ fullWidth
+ >
+ <Box>Mark as {isPendingCompletion ? 'completed' : 'done'}</Box>
+ </Button> */}
+ </Box>
+ </Sheet>
+ <Snackbar
+ open={isPendingCompletion}
+ endDecorator={
+ <Button
+ onClick={() => {
+ if (timeoutId) {
+ clearTimeout(timeoutId)
+ setIsPendingCompletion(false)
+ setTimeoutId(null)
+ setSecondsLeftToCancel(null) // Reset or adjust as needed
+ }
+ }}
+ size='md'
+ variant='outlined'
+ color='primary'
+ startDecorator={<CancelScheduleSend />}
+ >
+ Cancel
+ </Button>
+ }
+ >
+ <Typography level='body2' textAlign={'center'}>
+ Task will be marked as completed in {secondsLeftToCancel} seconds
+ </Typography>
+ </Snackbar>
+ </Container>
+ )
+}
+
+export default ChoreView
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
index ee25458..305f7c4 100644
--- a/src/views/Chores/ChoreCard.jsx
+++ b/src/views/Chores/ChoreCard.jsx
@@ -35,12 +35,13 @@ import moment from 'moment'
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from '../../Config'
-import writeToNFC from '../../service/NFCWriter'
+import { MarkChoreComplete } from '../../utils/Fetcher'
import { Fetch } from '../../utils/TokenManager'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
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 [activeUserId, setActiveUserId] = React.useState(0)
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
@@ -52,6 +53,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] =
React.useState(false)
const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
+ const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false)
const [anchorEl, setAnchorEl] = React.useState(null)
const menuRef = React.useRef(null)
const navigate = useNavigate()
@@ -116,9 +118,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}
const handleCompleteChore = () => {
- Fetch(`${API_URL}/chores/${chore.id}/do`, {
- method: 'POST',
- }).then(response => {
+ MarkChoreComplete(chore.id).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
@@ -323,7 +323,6 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}}
>
{getFrequencyIcon(chore)}
-
{getRecurrentChipText(chore)}
</div>
</Chip>
@@ -344,7 +343,13 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}}
>
<Grid container>
- <Grid item xs={9}>
+ <Grid
+ item
+ xs={9}
+ onClick={() => {
+ navigate(`/chores/${chore.id}`)
+ }}
+ >
{/* Box in top right with Chip showing next due date */}
<Box display='flex' justifyContent='start' alignItems='center'>
<Avatar sx={{ mr: 1, fontSize: 22 }}>
@@ -408,7 +413,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
disabled={isDisabled}
sx={{
borderRadius: '50%',
- width: 50,
+ minWidth: 50,
height: 50,
zIndex: 1,
}}
@@ -523,7 +528,8 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
<MenuItem
onClick={() => {
// write current chore URL to NFC
- writeToNFC(`${window.location.origin}/chores/${chore.id}`)
+ // writeToNFC(`${window.location.origin}/chores/${chore.id}`)
+ setIsNFCModalOpen(true)
}}
>
<Nfc />
@@ -581,6 +587,15 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
okText={'Complete'}
onSave={handleCompleteWithNote}
/>
+ <WriteNFCModal
+ config={{
+ isOpen: isNFCModalOpen,
+ url: `${window.location.origin}/chores/${chore.id}`,
+ onClose: () => {
+ setIsNFCModalOpen(false)
+ },
+ }}
+ />
</Card>
</>
)
diff --git a/src/views/Modals/Inputs/WriteNFCModal.jsx b/src/views/Modals/Inputs/WriteNFCModal.jsx
new file mode 100644
index 0000000..d71a2a3
--- /dev/null
+++ b/src/views/Modals/Inputs/WriteNFCModal.jsx
@@ -0,0 +1,109 @@
+import {
+ Box,
+ Button,
+ Checkbox,
+ ListItem,
+ Modal,
+ ModalDialog,
+ Typography,
+} from '@mui/joy'
+import React, { useState } from 'react'
+
+function WriteNFCModal({ config }) {
+ const [nfcStatus, setNfcStatus] = useState('idle') // 'idle', 'writing', 'success', 'error'
+ const [errorMessage, setErrorMessage] = useState('')
+ const [isAutoCompleteWhenScan, setIsAutoCompleteWhenScan] = useState(false)
+
+ const requestNFCAccess = async () => {
+ if ('NDEFReader' in window) {
+ // Assuming permission request is implicit in 'write' or 'scan' methods
+ setNfcStatus('idle')
+ } else {
+ alert('NFC is not supported by this browser.')
+ }
+ }
+
+ const writeToNFC = async url => {
+ if ('NDEFReader' in window) {
+ try {
+ const ndef = new window.NDEFReader()
+ await ndef.write({
+ records: [{ recordType: 'url', data: url }],
+ })
+ setNfcStatus('success')
+ } catch (error) {
+ console.error('Error writing to NFC tag:', error)
+ setNfcStatus('error')
+ setErrorMessage('Error writing to NFC tag. Please try again.')
+ }
+ } else {
+ setNfcStatus('error')
+ setErrorMessage('NFC is not supported by this browser.')
+ }
+ }
+
+ const handleClose = () => {
+ config.onClose()
+ setNfcStatus('idle')
+ setErrorMessage('')
+ }
+ const getURL = () => {
+ let url = config.url
+ if (isAutoCompleteWhenScan) {
+ url = url + '?auto_complete=true'
+ }
+
+ return url
+ }
+ return (
+ <Modal open={config?.isOpen} onClose={handleClose}>
+ <ModalDialog>
+ <Typography level='h4' mb={1}>
+ {nfcStatus === 'success' ? 'Success!' : 'Write to NFC'}
+ </Typography>
+
+ {nfcStatus === 'success' ? (
+ <Typography level='body-md' gutterBottom>
+ URL written to NFC tag successfully!
+ </Typography>
+ ) : (
+ <>
+ <Typography level='body-md' gutterBottom>
+ {nfcStatus === 'error'
+ ? errorMessage
+ : 'Press the button below to write to NFC.'}
+ </Typography>
+ <ListItem>
+ <Checkbox
+ checked={isAutoCompleteWhenScan}
+ onChange={e => setIsAutoCompleteWhenScan(e.target.checked)}
+ label='Auto-complete when scanned'
+ />
+ </ListItem>
+ <Box display={'flex'} justifyContent={'space-around'} mt={1}>
+ <Button
+ onClick={() => writeToNFC(getURL())}
+ fullWidth
+ sx={{ mr: 1 }}
+ disabled={nfcStatus === 'writing'}
+ >
+ Write NFC
+ </Button>
+ <Button onClick={requestNFCAccess} variant='outlined'>
+ Request Access
+ </Button>
+ </Box>
+ </>
+ )}
+
+ <Box display={'flex'} justifyContent={'center'} mt={2}>
+ <Button onClick={handleClose} variant='outlined'>
+ Close
+ </Button>
+ </Box>
+ </ModalDialog>
+ </Modal>
+ )
+}
+
+export default WriteNFCModal