diff --git a/components/Forms/LogHoursPopupWindowForm.tsx b/components/Forms/LogHoursPopupWindowForm.tsx index 7e245d8..5589318 100644 --- a/components/Forms/LogHoursPopupWindowForm.tsx +++ b/components/Forms/LogHoursPopupWindowForm.tsx @@ -5,43 +5,63 @@ import { LargeFormInput, LogHoursForm, } from '@/styles/components/Forms/logHoursPopupWindowForm.styles'; -import React from 'react'; +import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; import PopupWindow from '@/components/shared/PopupWindow'; import WindowFlow from '@/components/shared/WindowFlow'; +import VolunteerSignedEvents from '../Volunteer/VolunteerSignedEvents'; +import { QueriedVolunteerEventData } from 'bookem-shared/src/types/database'; const LogHoursPopupWindowForm = ({ setShowPopup, + successMessage, + errorMessage, }: { setShowPopup: React.Dispatch>; + successMessage: (message: string) => void; + errorMessage: (message: string) => void; }) => { // get functions from react hook form const { register, handleSubmit } = useForm(); + const [selectedEvent, setSelectedEvent] = + useState(); + // TODO: combine event and program into one so users only need to select event // that they signed up for (which is automatically under a program) - const pages = ['Event', 'Program', 'Numbers', 'Comments']; + const pages = ['Event', 'Numbers', 'Comments']; // handle form submission by parsing data and calling createVolunteerLog const onSubmit = (data: any) => { const results = JSON.stringify({ - // event: data.Event, + eventId: selectedEvent?._id, hours: parseInt(data.NumberOfHours), date: data.DateOfVisit, feedback: data.Comment, numBooks: data.NumberOfBooks, }); - console.log(results); createVolunteerLog(results); }; // sends a post request to insert the volunteer form const createVolunteerLog = async (data: any) => { - // TODO: implement endpoint for VolunteerEventApplication and call it - await fetch('/api/volunteerLogs/create', { - method: 'POST', - body: data, - }); + try { + // Implement endpoint for VolunteerEventApplication and call it + const response = await fetch('/api/volunteerLogs/create', { + method: 'POST', + body: data, + }); + if (response.status === 200) { + const message = (await response.json()).message; + setShowPopup(false); + successMessage(message); + } else { + const message = (await response.json()).message; + errorMessage(message); + } + } catch (err) { + errorMessage('Sorry an error occurred'); + } }; return ( @@ -52,29 +72,14 @@ const LogHoursPopupWindowForm = ({ components={[ // Page 1 - Event - TODO: show events volunteer signed up for + , - // Page 2 - Program - // TODO: remove this + // Page 2 - Numbers - Please select one program - - - Reading is Fundamental (RIF) - - - - Ready for Reading (RFR) - - - - Books for Nashville Kids (BFNK) - - , - - // Page 3 - Numbers - {/* TODO: Add an icon here to display tool tip */} Please log your volunteer hours
(rounded to the nearest @@ -90,7 +95,7 @@ const LogHoursPopupWindowForm = ({ Date of visit
, - // Page 4 - Comments - + // Page 3 - Comments + Anything else you'd like to share? { // set pop up window to false const [showPopup, setShowPopup] = useState(false); + const [messageApi, contextHolder] = message.useMessage(); + + // Display success message + const successMessage = (message: string) => { + messageApi.open({ + type: 'success', + content: message, + }); + }; + + // Display error message + const errorMessage = (message: string) => { + messageApi.open({ + type: 'error', + content: message, + }); + }; return ( <> + {/* Context for antd messages */} + {contextHolder} + {/* based on whether or not hideppopup is true, displays popup */} - {showPopup && } + {showPopup && ( + + )} Volunteer diff --git a/components/Volunteer/VolunteerSignedEvents.tsx b/components/Volunteer/VolunteerSignedEvents.tsx new file mode 100644 index 0000000..04510f3 --- /dev/null +++ b/components/Volunteer/VolunteerSignedEvents.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import { QueriedVolunteerEventData } from 'bookem-shared/src/types/database'; +import { fetchData } from '@/utils/utils'; +import { MainContainer } from '@/styles/volunteerHistory.styles'; +import SelectableLongEventCard from '../shared/SelectableLongEventCard'; + +/** + * format horizontal upcoming event scroll bar on home page + */ +const VolunteerSignedEvents = ({ + selectedEvent, + setSelectedEvent, +}: { + selectedEvent: QueriedVolunteerEventData | undefined; + setSelectedEvent: React.Dispatch< + React.SetStateAction + >; +}) => { + const [events, setEvents] = useState(); + const [error, setError] = useState(); + + const handleEventClick = (event: QueriedVolunteerEventData) => { + // Update the selected event when an event is clicked + setSelectedEvent(event); + }; + + // Fetch upcoming events when rendered + useEffect(() => { + fetchData('/api/events/log-hour') + .then(data => setEvents(data)) + .catch(err => setError(err)); + }, []); + return ( + <> + {/* TODO: render 404 page */} + {error && <>404 Event not found!} + {!events && !error &&
Loading...
} + {events && ( + + {/* Loop through each VolunteerEvent specific to that user */} + {events.map(event => ( + handleEventClick(event)} + /> + ))} + + )} + + ); +}; + +export default VolunteerSignedEvents; diff --git a/components/shared/LongEventCard.tsx b/components/shared/LongEventCard.tsx index 194cf53..e2cb528 100644 --- a/components/shared/LongEventCard.tsx +++ b/components/shared/LongEventCard.tsx @@ -15,22 +15,7 @@ import { } from '@/styles/components/longEventCard.styles'; import { QueriedVolunteerEventData } from 'bookem-shared/src/types/database'; import { convertLocationToString } from 'bookem-shared/src/utils/utils'; - -/** - * Helper function to format the time into a readable AM/PM format. - * - * Takes in an unformatted time and returns a formatted one. - */ -const formatAMPM = (date: { getHours: () => any; getMinutes: () => any }) => { - var hours = date.getHours(); - var minutes = date.getMinutes(); - var ampm = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; // the hour '0' should be '12' - minutes = minutes < 10 ? '0' + minutes : minutes; - var strTime = hours + ':' + minutes + ' ' + ampm; - return strTime; -}; +import { formatAMPM } from '@/utils/utils'; // this component takes in and displays all of an event's data const LongEventCard = ({ diff --git a/components/shared/SelectableLongEventCard.tsx b/components/shared/SelectableLongEventCard.tsx new file mode 100644 index 0000000..3aa3c9f --- /dev/null +++ b/components/shared/SelectableLongEventCard.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import Image from 'next/image'; +import { + EventImage, + Name, + AddressContainer, + AddressIcon, + InfoContainer, + Description, + ClockIcon, + Address, + CalendarIcon, + CheckmarkIcon, + Container, +} from '@/styles/components/longEventCard.styles'; +import { QueriedVolunteerEventData } from 'bookem-shared/src/types/database'; +import { convertLocationToString } from 'bookem-shared/src/utils/utils'; +import { formatAMPM } from '@/utils/utils'; + +// this component takes in and displays all of an event's data +const SelectableLongEventCard = ({ + eventData, + isSelected, + onClick, +}: { + eventData: QueriedVolunteerEventData; + isSelected: boolean; + onClick: () => void; +}) => { + // create a date object with JavaScript's Date constructor + const date = new Date(eventData.startDate); + + return ( + + + Event image icon + + {eventData.name} + + + + Map icon + +
{convertLocationToString(eventData.location)}
+
+ + + + Calendar icon + {/* calls a JavaScript method to format the date into a readable format */} + {date.toDateString()} + + + + Clock icon + {formatAMPM(date)} + + + + Checkmark icon + + {/* TODO: add data for number of books distributed*/}X books + distributed + + + +
+ ); +}; + +export default SelectableLongEventCard; diff --git a/pages/api/events/history.ts b/pages/api/events/history.ts index d760045..5594d83 100644 --- a/pages/api/events/history.ts +++ b/pages/api/events/history.ts @@ -40,6 +40,7 @@ export default async function handler( // get all volunteerEvents from collection that match the user's Id // sorted in descending order const user = await Users.findById(session.user._id); + if (!user) { return res.status(404).json({ message: 'User not found' }); } diff --git a/pages/api/events/log-hour.ts b/pages/api/events/log-hour.ts new file mode 100644 index 0000000..16bb466 --- /dev/null +++ b/pages/api/events/log-hour.ts @@ -0,0 +1,56 @@ +import dbConnect from '@/lib/dbConnect'; +import VolunteerEvents from 'bookem-shared/src/models/VolunteerEvents'; +import Users from 'bookem-shared/src/models/Users'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // Get request method + const { method } = req; + + const session = await getServerSession(req, res, authOptions); + + switch (method) { + /** + * @route GET /api/events/upcoming + * @desc Get all events in the future that the user is signed up for + * @res QueriedVolunteerEventData[] + */ + case 'GET': + try { + // const session = await getSession({ req }); + await dbConnect(); + // Fetch the user by ID to get their events array + // session.user._id shouldn't be null because we have the middleware to + // handle unauthenticated users + const user = await Users.findById(session.user._id); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + // Get events where start date is before today + const events = await VolunteerEvents.find({ + _id: { $in: user.events }, + startDate: { $lt: new Date() }, + }).sort({ startDate: 1 }); + + return res.status(200).json(events); + } catch (error) { + console.error(error); + res.status(500).json({ message: error }); + } + break; + + // case 'POST': + // case 'PUT': + // case 'DELETE': + default: + // res.setHeader('Allow', ['GET', 'PUT', 'DELETE', 'POST']); + res.status(405).end(`Method ${method} Not Allowed`); + break; + } +} diff --git a/pages/api/scripts/04-delete-logs.ts b/pages/api/scripts/04-delete-logs.ts new file mode 100644 index 0000000..51ed5cd --- /dev/null +++ b/pages/api/scripts/04-delete-logs.ts @@ -0,0 +1,32 @@ +import dbConnect from '@/lib/dbConnect'; +import VolunteerLogs from 'bookem-shared/src/models/VolunteerLogs'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + switch (req.method) { + case 'GET': + try { + // Connect to the database + await dbConnect(); + + // Delete all logs + await VolunteerLogs.deleteMany({}); + + res.status(200).json({ + success: true, + message: 'Successfully deleted all volunteer logs', + }); + } catch (error: any) { + res.status(500).json({ success: false, error: error.message }); + } + + break; + + default: + res.status(400).json({ success: false }); + break; + } +} diff --git a/pages/api/volunteerLogs/create.ts b/pages/api/volunteerLogs/create.ts index 75df408..cbeb15b 100644 --- a/pages/api/volunteerLogs/create.ts +++ b/pages/api/volunteerLogs/create.ts @@ -4,16 +4,12 @@ import type { NextApiRequest, NextApiResponse } from 'next'; // dbConnect is used to connect to our mongoDB database (via mongoose) import dbConnect from '@/lib/dbConnect'; -// getSession is used to get the user's session (if they are logged in) -import { getSession } from 'next-auth/react'; - // import the models and types we need import Users from 'bookem-shared/src/models/Users'; import VolunteerLogs from 'bookem-shared/src/models/VolunteerLogs'; -import { - VolunteerLogData, - QueriedUserData, -} from 'bookem-shared/src/types/database'; +import { VolunteerLogData } from 'bookem-shared/src/types/database'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '../auth/[...nextauth]'; /** * /api/volunteerLogs/create: @@ -32,51 +28,48 @@ export default async function handler( res: NextApiResponse ) { // check that user is authenticated - const session = await getSession({ req }); - - if (!session) { - res.status(401).json({ - error: 'You are unauthorized to perform this action. Please login first', - }); - return; - } - - // start a try catch block to catch any errors in parsing the request body - const volunteerLog = JSON.parse(req.body) as VolunteerLogData; - - if (!volunteerLog.hours) { - res.status(400).json({ message: 'Missing hours in request body.' }); - throw new Error('Invalid input. Missing hours in request body.'); - } + const session = await getServerSession(req, res, authOptions); - if (!volunteerLog.numBooks) { - res.status(400).json({ message: 'Missing numBooks in request body.' }); - throw new Error('Invalid input. Missing numBooks in request body.'); - } - - if (!volunteerLog.date) { - res.status(400).json({ message: 'Missing date in request body.' }); - throw new Error('Invalid input. Missing date in request body.'); - } + const validateData = (volunteerLog: VolunteerLogData) => { + if (!volunteerLog.eventId) { + res.status(400).json({ + message: 'You forgot to select an event.', + }); + return; + } + + if (!volunteerLog.hours) { + res + .status(400) + .json({ message: 'You forgot to fill in number of hours.' }); + return; + } + + if (!volunteerLog.numBooks) { + res + .status(400) + .json({ message: 'You forgot to fill in number of books donated' }); + return; + } + + if (!volunteerLog.date) { + res.status(400).json({ message: 'You forgot to fill in date' }); + return; + } + }; switch (req.method) { case 'POST': try { // connect to our database await dbConnect(); - const email = session.user?.email; - const user = (await Users.findOne({ - email: email, - })) as QueriedUserData; + // start a try catch block to catch any errors in parsing the request body + const volunteerLog = JSON.parse(req.body) as VolunteerLogData; - // If the user doesn't exist, return an error - if (!user) { - res.status(422).json({ message: 'This user does not exist' }); - throw new Error('This user does not exist'); - } + validateData(volunteerLog); - const usersId = user._id; + const usersId = session.user._id; // construct the object we want to insert into our database await VolunteerLogs.insertMany({ @@ -85,11 +78,9 @@ export default async function handler( }); // return the result of the action - res - .status(200) - .json( - 'Successfully inserted the log into the volunteerLogs collection' - ); + res.status(200).json({ + message: 'Successfully Logged hours', + }); } catch (e) { // if there is an error, print and return the error console.error('An error has occurred in volunteerLogs/create.ts', e); @@ -103,7 +94,7 @@ export default async function handler( default: res.status(405).json({ - error: 'Sorry, only POST requests are supported', + message: 'Sorry, only POST requests are supported', }); break; } diff --git a/utils/utils.ts b/utils/utils.ts index e6eeea6..9db3163 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -134,3 +134,22 @@ export const dateIsValid = (dateStr: string) => { return date.toISOString().startsWith(isoFormattedStr); }; + +/** + * Helper function to format the time into a readable AM/PM format. + * + * Takes in an unformatted time and returns a formatted one. + */ +export const formatAMPM = (date: { + getHours: () => any; + getMinutes: () => any; +}) => { + var hours = date.getHours(); + var minutes = date.getMinutes(); + var ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + minutes = minutes < 10 ? '0' + minutes : minutes; + var strTime = hours + ':' + minutes + ' ' + ampm; + return strTime; +};