From a0d7cb3d5a6e379e59c980095d5b083e818e60cb Mon Sep 17 00:00:00 2001 From: Muhammad Bilal Mohib-ul-Nabi Date: Sat, 22 Apr 2023 22:14:31 +0500 Subject: [PATCH] Invite member (#47) * Windows working * Please enter the commit message for your changes * added invite members dropdown and also the ability to send emails: --- check.js | 24 +++ .../CustomModal/InviteMembers/index.tsx | 186 +++++++----------- .../MultiSelectCustomAutoComplete/index.tsx | 153 ++++++++++---- .../List/MultiSelectChipDropDown/index.tsx | 4 +- lib/sendgrid.ts | 20 ++ package-lock.json | 10 + package.json | 2 + pages/api/send-invite.js | 61 ++++++ 8 files changed, 310 insertions(+), 150 deletions(-) create mode 100644 check.js create mode 100644 lib/sendgrid.ts create mode 100644 pages/api/send-invite.js diff --git a/check.js b/check.js new file mode 100644 index 0000000..bd13415 --- /dev/null +++ b/check.js @@ -0,0 +1,24 @@ +var nodemailer = require('nodemailer'); + +var transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: 'mbilals9922@gmail.com', + pass: '123321123BiLaL' + } +}); + +var mailOptions = { + from: 'mbilals9922@gmail.com', + to: 'bilalmohib7896@gmail.com', + subject: 'Sending Email using Node.js', + text: 'That was easy!' +}; + +transporter.sendMail(mailOptions, function(error, info){ + if (error) { + console.log(error); + } else { + console.log('Email sent: ' + info.response); + } +}); \ No newline at end of file diff --git a/components/CustomModal/InviteMembers/index.tsx b/components/CustomModal/InviteMembers/index.tsx index d2634e3..3513a9b 100644 --- a/components/CustomModal/InviteMembers/index.tsx +++ b/components/CustomModal/InviteMembers/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { Box, Button, @@ -59,13 +59,6 @@ const InviteMembers: React.FC = ( } } - // const projectMembers = [ - // "bialmohib7896@gmail.com", - // "mbilals9922@gmail.com", - // "2019cs682@gmail.com", - // "harisyounas@gmail.com" - // ]; - const [selectedMembers, setSelectedMembers] = React.useState([]); const [selectedProjects, setSelectedProjects] = React.useState([]); @@ -73,100 +66,73 @@ const InviteMembers: React.FC = ( // For popover const [anchorEl, setAnchorEl] = React.useState(null); - // const computerScienceProjects = [ - // { - // title: "Daraz Shopping App", - // // Remove spaces from title and make it lowercase - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "darazshoppingapp" - // }, - // { - // title: "Ecommerce Shopping App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "ecommerceshoppingapp" - // }, - // { - // title: "Covid-19 Tracker App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "covid19trackerapp" - // }, - // { - // title: "Social Media App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "socialmediaapp" - // }, - // { - // title: "Food Delivery App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "fooddeliveryapp" - // }, - // { - // title: "Uber Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "ubercloneapp" - // }, - // { - // title: "Netflix Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "netflixcloneapp" - // }, - // { - // title: "Tinder Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "tindercloneapp" - // }, - // { - // title: "Instagram Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "instagramcloneapp" - // }, - // { - // title: "Facebook Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "facebookcloneapp" - // }, - // { - // title: "Twitter Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "twittercloneapp" - // }, - // { - // title: "Whatsapp Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "whatsappcloneapp" - // }, - // { - // title: "Youtube Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "youtubecloneapp" - // }, - // { - // title: "Google Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "googlecloneapp" - // }, - // { - // title: "Amazon Clone App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "amazoncloneapp" - // }, - // { - // title: "Computer Vision App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "computervisionapp" - // }, - // { - // title: "Attendance Management App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "attendancemanagementapp" - // }, - // { - // title: "Ball Tracking App", - // // value: title.replace(/\s+/g, '').toLowerCase() - // value: "balltrackingapp" - // } - // ]; + // To get the value of the selected member + useEffect(() => { + console.log("selectedMembers", selectedMembers); + }, [selectedMembers]); + + const [modifiedProjectMembers, setModifiedProjectMembers] = React.useState([]); + + // Modify the projectMembers array and make it an array of objects such that it contains the name + // and the email of the member and extract first and last name first letter and make it the avatar + useEffect(() => { + const localModifiedProjectMembers = projectMembers.map((member: string) => { + const name = member.split("@")[0]; + const email = member; + const firstName = email.split(".")[0]; + const lastName = email.split(".")[1]; + // Extract first and last name first letter and make it the avatar + const avatar = firstName.charAt(0) + lastName.charAt(0); + return { + id: member, + name: member, + email: email, + avatar: avatar + } + }); + console.log("localModifiedProjectMembers", localModifiedProjectMembers); + + setModifiedProjectMembers(localModifiedProjectMembers); + + }, [projectMembers]); + + async function sendInvite(name: string, email: string) { + // name, email, link, projectName, senderName + const sendData = { + "name": "BILAL", + "email": "bilalmohib7896@gmail.com", + "link": "https://www.google.com", + "projectName": "FYP", + "senderName": "M. Bilal" + } + const response = await fetch('/api/send-invite', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(sendData), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message); + } + + return await response.json(); + } + + async function sendInvites() { + sendInvite('John Doe', 'john@example.com') + .then((response) => { + console.log(response.message); + alert(response.message); + }) + .catch((error) => { + console.error(error.message) + alert(error.message); + }); + } return ( @@ -174,7 +140,7 @@ const InviteMembers: React.FC = ( Email addresses - {/* = ( styles={styles.input} dropDownStyles={styles.dropDownStyles} /> */} - @@ -207,14 +175,6 @@ const InviteMembers: React.FC = ( - {/* */} = ( setSelectedArrayList={setSelectedProjects} styles={styles.input} dropDownStyles={styles.dropDownStyles} + type="projects" /> @@ -251,6 +212,7 @@ const InviteMembers: React.FC = ( color: "#fff", } }} + onClick={sendInvites} > Send diff --git a/components/MultiSelectCustomAutoComplete/index.tsx b/components/MultiSelectCustomAutoComplete/index.tsx index 95d475d..8bf0d61 100644 --- a/components/MultiSelectCustomAutoComplete/index.tsx +++ b/components/MultiSelectCustomAutoComplete/index.tsx @@ -12,7 +12,7 @@ interface MultiSelectCustomAutoCompleteProps { setSelectedArrayList?: any; styles?: any; dropDownStyles?: any; - + type: string } const MultiSelectCustomAutoComplete: FC = ({ @@ -21,25 +21,59 @@ const MultiSelectCustomAutoComplete: FC = ({ selectedArrayList, setSelectedArrayList, styles, - dropDownStyles + dropDownStyles, + type }) => { // ... - const [updatedOptions, setUpdatedOptions] = useState([]); + const [updatedOptions, setUpdatedOptions] = useState([]); + + const [lastInput, setLastInput] = useState(''); + + const [customOption, setCustomOption] = useState(null); + + const [error, setError] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const isValidEmail = (email: string) => { + const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(String(email).toLowerCase()); + }; useEffect(() => { const newOptions: any = []; for (let i = 0; i < options.length; i++) { - newOptions.push( - { - title: options[i].ProjectName, - value: options[i].id - } - ); + if (type === "members") { + newOptions.push( + { + title: options[i].name, + value: options[i].id + } + ); + } else if (type === "projects") { + newOptions.push( + { + title: options[i].ProjectName, + value: options[i].id + } + ); + } + } + + if (type === "members" && customOption) { + if (isValidEmail(customOption.actualTitle) === false) { + setError(true); + setErrorMessage("This email address cannot be added. Please enter a valid email address."); + return; + } else { + setError(false); + setErrorMessage(""); + newOptions.push(customOption); + } } setUpdatedOptions(newOptions); - }, [options]); + }, [options, customOption]); // Use 'updatedOptions' wherever you need the transformed options // ... @@ -47,6 +81,26 @@ const MultiSelectCustomAutoComplete: FC = ({ return ( { + if (type === "members") { + if (reason === 'reset') return; + + if (reason === 'input' && value.trim()) { + setLastInput(value.trim()); + setCustomOption({ + title: `${value.trim()}`, + actualTitle: value.trim(), + value: `custom-${Date.now()}`, + isCustom: true + }); + } else { + setCustomOption(null); + } + } + }} + + multiple options={updatedOptions} sx={(dropDownStyles) ? ( @@ -67,20 +121,35 @@ const MultiSelectCustomAutoComplete: FC = ({ } )} value={selectedArrayList} - onChange={(event, value) => { - if (value.length > 1) { - // Make a new array to extract the last element of the array and store in new array. Note it shold be a new array - // Not a invididual element of the array + onChange={(event, value, reason) => { + if (error) setError(false); + if (value.length > 0) { + let latestValue = value[value.length - 1]; - let newArray = value.slice(0, value.length - 1); + if (type === "members" && typeof latestValue === 'string') { + if (isValidEmail(lastInput)) { + const customOption = { + title: lastInput, + value: `custom-${Date.now()}`, + }; - let latestValue = value[value.length - 1].value; - console.log('Array new', newArray); - console.log('Latest value', latestValue); + value[value.length - 1] = customOption; + latestValue = customOption.value; + setError(false); + } else { + // Remove the invalid value + setError(true); + setErrorMessage("Please enter a valid email address") + alert("Please enter a valid email address") + value.pop(); + return; + } + } else { + latestValue = latestValue.value; + } - for (let i = 0; i < newArray.length; i++) { - if (latestValue === newArray[i].value) { - console.log('same', newArray[i].value); + for (let i = 0; i < value.length - 1; i++) { + if (latestValue === value[i].value) { return; } } @@ -89,7 +158,8 @@ const MultiSelectCustomAutoComplete: FC = ({ setSelectedArrayList(value); } }} - getOptionLabel={(option: any) => option.title} + + getOptionLabel={(option: any) => (typeof option === 'string' ? option : option.title)} filterSelectedOptions renderInput={(params) => ( = ({ placeholder={placeholder} /> )} - renderTags={(value: any[], getTagProps: any) => - value.map((option, index) => ( - - )) - } + renderTags={(value: any[], getTagProps: any) => { + return ( + <> + {value.map((option, index) => { + return ( + + ) + })} + + ) + }} /> + {(error && type === "members") && ( +
+ {errorMessage} +
+ )}
); } diff --git a/components/ProjectDetails/List/MultiSelectChipDropDown/index.tsx b/components/ProjectDetails/List/MultiSelectChipDropDown/index.tsx index 48395e0..e8f387d 100644 --- a/components/ProjectDetails/List/MultiSelectChipDropDown/index.tsx +++ b/components/ProjectDetails/List/MultiSelectChipDropDown/index.tsx @@ -70,7 +70,7 @@ const MultiSelectChipDropDown: FC = ({ input={} renderValue={(selected) => { if (selected.length === 0) { - return {placeholder}; + return {placeholder}; } return ( @@ -101,7 +101,7 @@ const MultiSelectChipDropDown: FC = ({ inputProps={{ 'aria-label': 'Without label' }} > - {placeholder} + {placeholder} {options.map((option: string) => ( { + // @ts-ignore + sgMail.setApiKey(process.env.SENDGRID_API_KEY); + + const msg = { + to, + from: 'your-email@example.com', + subject, + text, + }; + + try { + await sgMail.send(msg); + console.log(`Email sent to ${to}`); + } catch (error) { + console.error(error); + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8e96af5..e2666a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1578,6 +1578,11 @@ "update-browserslist-db": "^1.0.10" } }, + "calendar-js": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/calendar-js/-/calendar-js-1.4.4.tgz", + "integrity": "sha512-KesBEbMZnjGzO/xl0ZcJVnezSRw3Xn/KaSUneY4LnANKlZwlllOtd6UGSyW8DSM+diUNi5dVVmSx4eZIMAA2Aw==" + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3680,6 +3685,11 @@ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true }, + "ramda": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", + "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==" + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", diff --git a/package.json b/package.json index 8aae8ed..a9ba6fa 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@mui/icons-material": "^5.11.0", "@mui/lab": "^5.0.0-alpha.127", "@mui/material": "^5.11.8", + "@sendgrid/mail": "^7.7.0", "@mui/x-date-pickers": "^6.2.1", "calendar-js": "^1.4.4", "chart.js": "^2.9.4", @@ -23,6 +24,7 @@ "formik": "^2.2.9", "framer-motion": "^10.11.2", "next": "12.3.1", + "nodemailer": "^6.9.1", "ramda": "^0.29.0", "react": "18.2.0", "react-calendar": "^4.2.1", diff --git a/pages/api/send-invite.js b/pages/api/send-invite.js new file mode 100644 index 0000000..0dbea82 --- /dev/null +++ b/pages/api/send-invite.js @@ -0,0 +1,61 @@ +import nodemailer from "nodemailer"; + +export default async function handler(req, res) { + if (req.method === "POST") { + const { name, email, link, projectName, senderName } = req.body; + + // Set up Nodemailer and SMTP configuration + // const transporter = nodemailer.createTransport({ + // host: process.env.SMTP_HOST, + // port: process.env.SMTP_PORT, + // secure: process.env.SMTP_SECURE === "true", + // auth: { + // user: process.env.SMTP_USER, + // pass: process.env.SMTP_PASSWORD, + // }, + // }); + // Set up Nodemailer and Gmail SMTP configuration + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: "mbilals9922@gmail.com", + pass: "123321123BiLaL", + }, + }); + + // Define the email template + const emailTemplate = ` +

Welcome to Our TaskEncher!

+

Hello, Dear ${name},

+

We are excited to have you on board. + You have been invited to join our new project named ${projectName}.
+ Please find the details of the project below:

+ +

Project Name: ${projectName}

+

WorkSpace Link: ${link}

+

Looking forward to working with you!

+

Best Regards,

+

Your TaskEncher Team

+ `; + + // Set up email options + const mailOptions = { + // from: process.env.SMTP_FROM, + from: "mbilals9922@gmail.com", + to: "bilalmohib7896@gmail.com", + subject: `Action Required: ${senderName} invited you to join a WorkSpace in TaskEncher`, + html: emailTemplate, + }; + + // Send email + try { + await transporter.sendMail(mailOptions); + res.status(200).json({ message: "Email sent successfully" }); + } catch (error) { + console.error('Error sending email:', error); // Log the error to the console + res.status(500).json({ error: error.message }); + } + } else { + res.status(405).json({ message: "Method Not Allowed" }); + } +}