From 9a07689dfeb736341b4f1b378e0ec758ea9cd0ff Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sat, 6 Jul 2024 02:33:06 -0400 Subject: feat: Add NFC tag writing functionality to ChoreCard component, Add Email to sign up --- src/contexts/RouterContext.jsx | 5 + src/utils/Fetcher.jsx | 15 ++ src/views/Authorization/LoginView.jsx | 11 +- src/views/ChoreEdit/ChoreView.jsx | 292 ++++++++++++++++++++++++++++++ src/views/Chores/ChoreCard.jsx | 31 +++- src/views/Modals/Inputs/WriteNFCModal.jsx | 109 +++++++++++ 6 files changed, 452 insertions(+), 11 deletions(-) create mode 100644 src/views/ChoreEdit/ChoreView.jsx create mode 100644 src/views/Modals/Inputs/WriteNFCModal.jsx 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' @@ -41,6 +42,10 @@ const Router = createBrowserRouter([ path: '/chores/:choreId/edit', element: , }, + { + path: '/chores/:choreId', + element: , + }, { path: '/chores/create', element: , 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: , + text: 'Due Date', + subtext: moment(chore.dueDate).format('MM/DD/YYYY hh:mm A'), + }, + { + icon: , + text: 'Assigned To', + subtext: performers.find(p => p.id === chore.assignedTo)?.displayName, + }, + { + icon: , + text: 'Created By', + subtext: performers.find(p => p.id === chore.createdBy)?.displayName, + }, + // { + // icon: , + // text: 'Frequency', + // subtext: + // chore.frequencyType.charAt(0).toUpperCase() + + // chore.frequencyType.slice(1), + // }, + { + icon: , + text: 'Total Completed', + subtext: `${chore.totalCompletedCount}`, + }, + // { + // icon: , + // text: 'Last Completed', + // subtext: + // chore.lastCompletedDate && + // moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A'), + // }, + { + icon: , + 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 ( + + + + + {chore.name} + + + + {infoCards.map((info, index) => ( + + + + + {info.icon} + + + + {info.text} + + + {info.subtext ? info.subtext : '--'} + + + + + + ))} + + + + + {/* */} + + + { + if (timeoutId) { + clearTimeout(timeoutId) + setIsPendingCompletion(false) + setTimeoutId(null) + setSecondsLeftToCancel(null) // Reset or adjust as needed + } + }} + size='md' + variant='outlined' + color='primary' + startDecorator={} + > + Cancel + + } + > + + Task will be marked as completed in {secondsLeftToCancel} seconds + + + + ) +} + +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)} @@ -344,7 +343,13 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => { }} > - + { + navigate(`/chores/${chore.id}`) + }} + > {/* Box in top right with Chip showing next due date */} @@ -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 }) => { { // write current chore URL to NFC - writeToNFC(`${window.location.origin}/chores/${chore.id}`) + // writeToNFC(`${window.location.origin}/chores/${chore.id}`) + setIsNFCModalOpen(true) }} > @@ -581,6 +587,15 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => { okText={'Complete'} onSave={handleCompleteWithNote} /> + { + setIsNFCModalOpen(false) + }, + }} + /> ) 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 ( + + + + {nfcStatus === 'success' ? 'Success!' : 'Write to NFC'} + + + {nfcStatus === 'success' ? ( + + URL written to NFC tag successfully! + + ) : ( + <> + + {nfcStatus === 'error' + ? errorMessage + : 'Press the button below to write to NFC.'} + + + setIsAutoCompleteWhenScan(e.target.checked)} + label='Auto-complete when scanned' + /> + + + + + + + )} + + + + + + + ) +} + +export default WriteNFCModal -- cgit