diff --git a/backend/package-lock.json b/backend/package-lock.json index 975271d..b395a8a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6507,16 +6507,6 @@ "node": ">= 0.8" } }, - "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -9853,8 +9843,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "8.2.0", @@ -10746,15 +10735,13 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "requires": {} + "dev": true }, "eslint-config-standard": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", - "dev": true, - "requires": {} + "dev": true }, "eslint-config-standard-with-typescript": { "version": "23.0.0", @@ -10915,8 +10902,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "7.1.1", @@ -12564,12 +12550,6 @@ "unpipe": "1.0.0" } }, - "react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "peer": true - }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -13260,8 +13240,7 @@ "use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", - "requires": {} + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==" }, "util": { "version": "0.10.4", diff --git a/backend/routes/emailRoutes.js b/backend/routes/emailRoutes.js index 4ab6ee2..800dbac 100644 --- a/backend/routes/emailRoutes.js +++ b/backend/routes/emailRoutes.js @@ -1,74 +1,75 @@ const express = require("express"); const nodemailer = require("nodemailer"); const multer = require("multer"); -const router = express.Router() +const router = express.Router(); -require("dotenv/config") +require("dotenv/config"); // POST /api/email/ -router.post('/', async (req, res) => { +router.post("/", async (req, res) => { try { const { recipientEmail, subject, body, isHTML } = req.body; const transporter = nodemailer.createTransport({ - service: 'Gmail', + service: "Gmail", auth: { - user: process.env.email_address, - pass: process.env.email_password, + user: process.env.H4H_EMAIL, + pass: process.env.H4H_EMAIL_PASSWORD, }, }); - let mailOptions = '' + let mailOptions = ""; // Prepare the email message if (isHTML) { mailOptions = { - from: process.env.email_address, + from: process.env.H4H_EMAIL, to: recipientEmail, - subject: subject , + subject: subject, html: body, // Use the provided body as HTML }; } else { mailOptions = { - from: process.env.email_address, + from: process.env.H4H_EMAIL, to: recipientEmail, - subject: subject , + subject: subject, text: body, // Use the provided body as plain text }; } // Send the email await transporter.sendMail(mailOptions); - - res.status(200).json({ message: 'Custom email sent successfully' }); // Return a success response + + res.status(200).json({ message: "Custom email sent successfully" }); // Return a success response } catch (error) { - console.log(error) - res.status(500).json({ error: 'An error occurred while sending the custom email' }); // Return an error response + console.log(error); + res + .status(500) + .json({ error: "An error occurred while sending the custom email" }); // Return an error response } }); - // POST /api/email/attach-files const storage = multer.memoryStorage(); // Create a Multer storage configuration const upload = multer({ storage }); -router.post('/attach-files', upload.array('attachments'), async (req, res) => { +router.post("/attach-files", upload.array("attachments"), async (req, res) => { try { const { recipientEmail, subject, body, isHTML } = req.body; const attachments = req.files; const transporter = nodemailer.createTransport({ - service: 'Gmail', + service: "Gmail", auth: { - user: process.env.email_address, - pass: process.env.email_password, + user: process.env.H4H_EMAIL, + pass: process.env.H4H_EMAIL_PASSWORD, }, }); - let mailOptions = ''; + let mailOptions = ""; // Prepare the email message if (isHTML) { mailOptions = { - from: process.env.email_address, + from: process.env.H4H_EMAIL, to: recipientEmail, subject: subject, html: body, // Use the provided body as HTML @@ -80,7 +81,7 @@ router.post('/attach-files', upload.array('attachments'), async (req, res) => { }; } else { mailOptions = { - from: process.env.email_address, + from: process.env.H4H_EMAIL, to: recipientEmail, subject: subject, text: body, // Use the provided body as plain text @@ -95,32 +96,34 @@ router.post('/attach-files', upload.array('attachments'), async (req, res) => { // Send the email await transporter.sendMail(mailOptions); - res.status(200).json({ message: 'Custom email sent successfully' }); // Return a success response + res.status(200).json({ message: "Custom email sent successfully" }); // Return a success response } catch (error) { console.log(error); - res.status(500).json({ error: 'An error occurred while sending the custom email' }); // Return an error response + res + .status(500) + .json({ error: "An error occurred while sending the custom email" }); // Return an error response } }); // POST /api/email/donation-approved-scheduled -router.post('/donation-approved-scheduled', async (req, res) => { +router.post("/donation-approved-scheduled", async (req, res) => { try { const { recipientEmail, donationDetails } = req.body; // Create a Nodemailer transporter const transporter = nodemailer.createTransport({ - service: 'Gmail', + service: "Gmail", auth: { - user: process.env.email_address, - pass: process.env.email_password, + user: process.env.H4H_EMAIL, + pass: process.env.H4H_EMAIL_PASSWORD, }, }); // Prepare the email message const mailOptions = { - from: process.env.email_address, + from: process.env.H4H_EMAIL, to: recipientEmail, - subject: 'Donation Approved & Scheduled', + subject: "Donation Approved & Scheduled", html: `
${donationDetails.name},
Thank you for your donation!
Your donation has been approved and scheduled for pickup or drop-off. Here are the details:
@@ -136,12 +139,17 @@ router.post('/donation-approved-scheduled', async (req, res) => { await transporter.sendMail(mailOptions); // Return a success response - res.status(200).json({ message: 'Donation approval and schedule email sent successfully' }); + res.status(200).json({ + message: "Donation approval and schedule email sent successfully", + }); } catch (error) { - console.error('Error sending donation approval and schedule email:', error); + console.error("Error sending donation approval and schedule email:", error); // Return an error response - res.status(500).json({ error: 'An error occurred while sending the donation approval and schedule email' }); + res.status(500).json({ + error: + "An error occurred while sending the donation approval and schedule email", + }); } }); -module.exports = router \ No newline at end of file +module.exports = router; diff --git a/frontend/src/api/email.ts b/frontend/src/api/email.ts new file mode 100644 index 0000000..cae6a2c --- /dev/null +++ b/frontend/src/api/email.ts @@ -0,0 +1,52 @@ +const emailURL = "http://localhost:3001/api/email/"; +/* ----------------------POST Requests---------------------------*/ + +// Email data model +export interface Email { + recipientEmail: string; + subject: string; + body: string | HTMLElement; + isHTML: boolean; +} + +// Send an email without attachment +export const sendEmail = async (email: Email) => + fetch(emailURL, { + headers: { + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + recipientEmail: email.recipientEmail, + subject: email.subject, + body: email.body, + isHTML: email.isHTML, + }), + }) + .then(async (res) => { + const response = await res.json(); + if (!res.ok) { + // check server response + throw new Error(`${res.status}-${res.statusText}`); + } + return response; + }) + .catch((error) => console.error("Error: ", error)); // handle error + +/* --------------Functions for Creating HTMLElements-------------- */ +export const createParagraph = (text: string) => { + const paragraph = document.createElement("p"); + paragraph.style.fontFamily = "Rubik, sans-serif"; + paragraph.innerText = text; + return paragraph; +}; + +export const createListItem = (label: string, value: any) => { + const listItem = document.createElement("li"); + const strong = document.createElement("strong"); + strong.style.fontFamily = "Rubik, sans-serif"; + strong.innerText = label; + listItem.appendChild(strong); + listItem.appendChild(document.createTextNode(String(value))); + return listItem; +}; diff --git a/frontend/src/app/Donor/History/DonationInfo/[[...slug]]/page.tsx b/frontend/src/app/Donor/History/DonationInfo/[[...slug]]/page.tsx index 8b27c5b..9308ae7 100644 --- a/frontend/src/app/Donor/History/DonationInfo/[[...slug]]/page.tsx +++ b/frontend/src/app/Donor/History/DonationInfo/[[...slug]]/page.tsx @@ -6,6 +6,7 @@ import PropTypes from "prop-types"; import Box from "@mui/material/Box"; import { getUserByID, User } from "api/user"; import { getItemByID, Item, updateItem } from "api/item"; +import { Email, sendEmail, createParagraph, createListItem } from "api/email"; import Tab from "@mui/material/Tab"; import Tabs from "@mui/material/Tabs"; import Typography from "@mui/material/Typography"; @@ -144,6 +145,7 @@ async function DonationInfoPage({ )) && (await sendUpdatedItemToDB("Approved and Scheduled", true)) ) { + sendDonationStatusEmail(true); console.log("Success submitting events!"); clearTimeSlots(); // Clear time slots from redux router.push(nextPath); @@ -191,9 +193,115 @@ async function DonationInfoPage({ // "There was an error updating the item. Please try again later." // TODO: add error message to user } + setItem(updatedItem); // Update state of current item return response; }; + const sendDonationStatusEmail = async (isApproved: boolean) => { + try { + const subject = isApproved + ? "Donation Request Approval" + : "Donation Request Rejection"; + + const status = isApproved ? "Approved" : "Rejected"; + + const container = document.createElement("div"); + + container.appendChild( + createParagraph( + isApproved + ? "Your donation request has been approved." + : "Your donation request has been rejected." + ) + ); + container.appendChild( + createParagraph("Here are the details of the item:") + ); + + const list = document.createElement("ul"); + list.style.listStyleType = "none"; + container.appendChild(list); + + const stateValue = item.state || ""; // Check for undefined state + + // TODO: Include time for pickup if + const listItems = [ + { label: "Name: ", value: item.name }, + { label: "Size: ", value: item.size }, + { label: "Address: ", value: item.address }, + { label: "City: ", value: item.city }, + { label: "State: ", value: stateValue }, + { label: "Zip Code: ", value: item.zipCode }, + { label: "Scheduling: ", value: item.scheduling }, + ]; + + listItems.forEach((item) => { + const listItem = createListItem(item.label, item.value); + list.appendChild(listItem); + }); + + // TODO: Include which time was approved + if (item.timeAvailability) { + const timeAvailability = document.createElement("li"); + const strong = document.createElement("strong"); + strong.style.fontFamily = "Rubik, sans-serif"; + strong.innerText = "Time Availability: "; + timeAvailability.appendChild(strong); + + const ul = document.createElement("ul"); + + item.timeAvailability.forEach((time, index) => { + const li = document.createElement("li"); + const start = document.createElement("span"); + start.innerText = `Start: ${new Date(time.start).toLocaleString( + "en-US", + { + timeZone: "America/Los_Angeles", + } + )}`; + li.appendChild(start); + li.appendChild(document.createElement("br")); + const end = document.createElement("span"); + end.innerText = `End: ${new Date(time.end).toLocaleString("en-US", { + timeZone: "America/Los_Angeles", + })}`; + li.appendChild(end); + ul.appendChild(li); + }); + + timeAvailability.appendChild(ul); + list.appendChild(timeAvailability); + } + + const stat = createListItem( + "Status: ", + isApproved ? "Approved" : "Rejected" + ); + list.appendChild(stat); + + container.appendChild( + createParagraph("Thank you for supporting our cause!") + ); + + const email = { + recipientEmail: donor.email, + subject, + body: container.outerHTML, + isHTML: true, + }; + + const response = await sendEmail(email); + if (!response) { + throw new Error("There was an error sending the donation email."); + } + + return response; + } catch (error) { + console.error("Error sending donation email:", error); + throw error; + } + }; + // Fetch and set item on load useEffect(() => { const fetchedItem = diff --git a/frontend/src/components/donor/donation/SubmitInfo.tsx b/frontend/src/components/donor/donation/SubmitInfo.tsx index 1861688..96a3df6 100644 --- a/frontend/src/components/donor/donation/SubmitInfo.tsx +++ b/frontend/src/components/donor/donation/SubmitInfo.tsx @@ -6,6 +6,8 @@ import { useRouter } from "next/navigation"; import DonatorNavbar from "components/donor/DonorNavbar/DonorNavbar"; import ProgressBar from "components/donor/donation/ProgressBar"; import { useSelector, useDispatch } from "react-redux"; +import { getUserByID } from "api/user"; +import { Email, sendEmail, createParagraph, createListItem } from "api/email"; import { Item, addItem } from "../../../api/item"; import { addImages, getImages, getImageByID } from "../../../api/image"; import { RootState } from "../../../redux/store"; @@ -144,6 +146,121 @@ const SubmitInfo: React.FC