diff options
author | Mo Tarbin <mhed.t91@gmail.com> | 2024-07-06 02:33:06 -0400 |
---|---|---|
committer | Mo Tarbin <mhed.t91@gmail.com> | 2024-07-06 02:33:06 -0400 |
commit | 9a07689dfeb736341b4f1b378e0ec758ea9cd0ff (patch) | |
tree | 0fe096809a9cc6baaa0052c091a3a60766274224 /src | |
parent | c34da50c8c1564c9c6556522bf12a93639c1f160 (diff) | |
download | donetick-frontend-9a07689dfeb736341b4f1b378e0ec758ea9cd0ff.tar.gz donetick-frontend-9a07689dfeb736341b4f1b378e0ec758ea9cd0ff.tar.bz2 donetick-frontend-9a07689dfeb736341b4f1b378e0ec758ea9cd0ff.zip |
feat: Add NFC tag writing functionality to ChoreCard component, Add Email to sign up
Diffstat (limited to 'src')
-rw-r--r-- | src/contexts/RouterContext.jsx | 5 | ||||
-rw-r--r-- | src/utils/Fetcher.jsx | 15 | ||||
-rw-r--r-- | src/views/Authorization/LoginView.jsx | 11 | ||||
-rw-r--r-- | src/views/ChoreEdit/ChoreView.jsx | 292 | ||||
-rw-r--r-- | src/views/Chores/ChoreCard.jsx | 31 | ||||
-rw-r--r-- | src/views/Modals/Inputs/WriteNFCModal.jsx | 109 |
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 |