feat: Add NFC tag writing functionality to ChoreCard component, Add Email to sign up

This commit is contained in:
Mo Tarbin 2024-07-06 02:33:06 -04:00
parent c34da50c8c
commit 9a07689dfe
6 changed files with 452 additions and 11 deletions

View file

@ -8,6 +8,7 @@ import ForgotPasswordView from '../views/Authorization/ForgotPasswordView'
import LoginView from '../views/Authorization/LoginView' import LoginView from '../views/Authorization/LoginView'
import SignupView from '../views/Authorization/Signup' import SignupView from '../views/Authorization/Signup'
import UpdatePasswordView from '../views/Authorization/UpdatePasswordView' import UpdatePasswordView from '../views/Authorization/UpdatePasswordView'
import ChoreView from '../views/ChoreEdit/ChoreView'
import MyChores from '../views/Chores/MyChores' import MyChores from '../views/Chores/MyChores'
import JoinCircleView from '../views/Circles/JoinCircle' import JoinCircleView from '../views/Circles/JoinCircle'
import ChoreHistory from '../views/History/ChoreHistory' import ChoreHistory from '../views/History/ChoreHistory'
@ -41,6 +42,10 @@ const Router = createBrowserRouter([
path: '/chores/:choreId/edit', path: '/chores/:choreId/edit',
element: <ChoreEdit />, element: <ChoreEdit />,
}, },
{
path: '/chores/:choreId',
element: <ChoreView />,
},
{ {
path: '/chores/create', path: '/chores/create',
element: <ChoreEdit />, element: <ChoreEdit />,

View file

@ -51,6 +51,19 @@ const GetChoreByID = id => {
headers: HEADERS(), 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 => { const CreateChore = chore => {
return Fetch(`${API_URL}/chores/`, { return Fetch(`${API_URL}/chores/`, {
method: 'POST', method: 'POST',
@ -238,6 +251,7 @@ export {
GetAllCircleMembers, GetAllCircleMembers,
GetAllUsers, GetAllUsers,
GetChoreByID, GetChoreByID,
GetChoreDetailById,
GetChoreHistory, GetChoreHistory,
GetChores, GetChores,
GetCircleMemberRequests, GetCircleMemberRequests,
@ -250,6 +264,7 @@ export {
JoinCircle, JoinCircle,
LeaveCircle, LeaveCircle,
login, login,
MarkChoreComplete,
SaveChore, SaveChore,
SaveThing, SaveThing,
signUp, signUp,

View file

@ -62,7 +62,6 @@ const LoginView = () => {
} }
const loggedWithProvider = function (provider, data) { const loggedWithProvider = function (provider, data) {
console.log(provider, data)
return fetch(API_URL + `/auth/${provider}/callback`, { return fetch(API_URL + `/auth/${provider}/callback`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -80,8 +79,14 @@ const LoginView = () => {
return response.json().then(data => { return response.json().then(data => {
localStorage.setItem('ca_token', data.token) localStorage.setItem('ca_token', data.token)
localStorage.setItem('ca_expiration', data.expire) 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 => { return response.json().then(error => {

View file

@ -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

View file

@ -35,12 +35,13 @@ import moment from 'moment'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { API_URL } from '../../Config' import { API_URL } from '../../Config'
import writeToNFC from '../../service/NFCWriter' import { MarkChoreComplete } from '../../utils/Fetcher'
import { Fetch } from '../../utils/TokenManager' import { Fetch } from '../../utils/TokenManager'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import DateModal from '../Modals/Inputs/DateModal' 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'
const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => { const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
const [activeUserId, setActiveUserId] = React.useState(0) const [activeUserId, setActiveUserId] = React.useState(0)
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] = const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
@ -52,6 +53,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] = const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] =
React.useState(false) React.useState(false)
const [confirmModelConfig, setConfirmModelConfig] = React.useState({}) const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false)
const [anchorEl, setAnchorEl] = React.useState(null) const [anchorEl, setAnchorEl] = React.useState(null)
const menuRef = React.useRef(null) const menuRef = React.useRef(null)
const navigate = useNavigate() const navigate = useNavigate()
@ -116,9 +118,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
} }
const handleCompleteChore = () => { const handleCompleteChore = () => {
Fetch(`${API_URL}/chores/${chore.id}/do`, { MarkChoreComplete(chore.id).then(response => {
method: 'POST',
}).then(response => {
if (response.ok) { if (response.ok) {
response.json().then(data => { response.json().then(data => {
const newChore = data.res const newChore = data.res
@ -323,7 +323,6 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}} }}
> >
{getFrequencyIcon(chore)} {getFrequencyIcon(chore)}
{getRecurrentChipText(chore)} {getRecurrentChipText(chore)}
</div> </div>
</Chip> </Chip>
@ -344,7 +343,13 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}} }}
> >
<Grid container> <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 in top right with Chip showing next due date */}
<Box display='flex' justifyContent='start' alignItems='center'> <Box display='flex' justifyContent='start' alignItems='center'>
<Avatar sx={{ mr: 1, fontSize: 22 }}> <Avatar sx={{ mr: 1, fontSize: 22 }}>
@ -408,7 +413,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
disabled={isDisabled} disabled={isDisabled}
sx={{ sx={{
borderRadius: '50%', borderRadius: '50%',
width: 50, minWidth: 50,
height: 50, height: 50,
zIndex: 1, zIndex: 1,
}} }}
@ -523,7 +528,8 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
<MenuItem <MenuItem
onClick={() => { onClick={() => {
// write current chore URL to NFC // write current chore URL to NFC
writeToNFC(`${window.location.origin}/chores/${chore.id}`) // writeToNFC(`${window.location.origin}/chores/${chore.id}`)
setIsNFCModalOpen(true)
}} }}
> >
<Nfc /> <Nfc />
@ -581,6 +587,15 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
okText={'Complete'} okText={'Complete'}
onSave={handleCompleteWithNote} onSave={handleCompleteWithNote}
/> />
<WriteNFCModal
config={{
isOpen: isNFCModalOpen,
url: `${window.location.origin}/chores/${chore.id}`,
onClose: () => {
setIsNFCModalOpen(false)
},
}}
/>
</Card> </Card>
</> </>
) )

View file

@ -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