From 3bb32eb2e9fe50c1a0fbf223aa0ced5614ac8ce6 Mon Sep 17 00:00:00 2001 From: Mohammed JBILOU <95973236+Molaryy@users.noreply.github.com> Date: Sun, 1 Oct 2023 12:59:11 +0200 Subject: [PATCH] feat: add mails can be sent to anothe users with gmail account --- package.json | 4 + pages/api/email.ts | 57 + pages/api/program/create.ts | 30 +- src/components/dashboardPage/FileOptions.tsx | 202 +- src/components/file/RenameFile.tsx | 57 +- src/components/file/SendEmailFile.tsx | 206 + src/config/nodemailer.ts | 17 + src/lib/email.ts | 14 + src/lib/services/email.ts | 0 yarn.lock | 11489 +++++++++-------- 10 files changed, 6463 insertions(+), 5613 deletions(-) create mode 100644 pages/api/email.ts create mode 100644 src/components/file/SendEmailFile.tsx create mode 100644 src/config/nodemailer.ts create mode 100644 src/lib/email.ts create mode 100644 src/lib/services/email.ts diff --git a/package.json b/package.json index e939d3b7..bbe39bf5 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@chakra-ui/styled-system": "^2.9.1", "@chakra-ui/system": "^2.5.8", "@chakra-ui/theme-tools": "^2.0.18", + "@elasticemail/elasticemail-client": "^4.0.23", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "aleph-sdk-ts": "^3.2.0", @@ -22,9 +23,11 @@ "js-file-download": "^0.4.12", "next": "^12.3.4", "next-auth": "4.10.3", + "nodemailer": "^6.9.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.10.1", + "smtpjs": "^0.0.1", "typescript": "^5.1.3", "zod": "^3.21.4" }, @@ -54,6 +57,7 @@ "@types/crypto-js": "^4.1.1", "@types/git-clone": "^0.2.0", "@types/node": "^16.18.31", + "@types/nodemailer": "^6.4.11", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.60.0", diff --git a/pages/api/email.ts b/pages/api/email.ts new file mode 100644 index 00000000..2b060688 --- /dev/null +++ b/pages/api/email.ts @@ -0,0 +1,57 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { mailOptions, transporter } from '../../src/config/nodemailer'; + +type BodyEmail = { + email: string, + subject: string, + message: string, +} + +const CONTACT_MESSAGE_FIELDS: BodyEmail = { + email: 'Email', + subject: 'Subject', + message: 'Message', +}; + +const generateEmailContent = (data: BodyEmail): { text: string, html: string } => { + const stringData = Object.entries(data).reduce((str: string, [key, val]) => { + if (CONTACT_MESSAGE_FIELDS[key as keyof BodyEmail]) { + return `${str}${CONTACT_MESSAGE_FIELDS[key as keyof BodyEmail]}: \n${val} \n \n`; + } + return str; + }, ''); + const htmlData = Object.entries(data).reduce((str, [key, val]) => (`${str}

${CONTACT_MESSAGE_FIELDS[key as keyof BodyEmail]}

${val}

`), ''); + + return { + text: stringData, + html: `

New Contact Message

${htmlData}
`, + }; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { + if (req.method === 'POST') { + const data = req.body; + if (!data || !data.email) { + return res.status(400).send({ message: 'Bad request' }); + } + + try { + mailOptions.to = data.email; + await transporter.sendMail({ + ...mailOptions, + ...generateEmailContent(data), + subject: data.subject, + }); + + return res.status(200).json({ success: true }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + } catch (err: Error) { + return res.status(400).json({ message: err.message }); + } + } + if (req.method === 'GET') { + return res.status(200).json({ success: 'Bien vu' }); + } + return res.status(200).json({ success: 'Bien return' }); +}; diff --git a/pages/api/program/create.ts b/pages/api/program/create.ts index 168bd83c..05f3802c 100644 --- a/pages/api/program/create.ts +++ b/pages/api/program/create.ts @@ -1,24 +1,24 @@ import z from 'zod'; import deploy from 'lib/services/deploy'; -import { clone, getProgramName } from 'lib/services/git'; -import { NextApiRequest, NextApiResponse } from 'next'; +import {clone, getProgramName} from 'lib/services/git'; +import {NextApiRequest, NextApiResponse} from 'next'; const postSchema = z.object({ - // eslint-disable-next-line no-useless-escape - repository: z.string().regex(/((git|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/), - entrypoint: z.string(), + // eslint-disable-next-line no-useless-escape + repository: z.string().regex(/((git|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/), + entrypoint: z.string(), }); export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (req.method !== 'POST') throw new Error('Method not allowed'); - const body = postSchema.parse(req.body); - const { repository, entrypoint } = body; - const path = await clone(repository); - const itemHash = await deploy(path, entrypoint); - return res.status(200).json({ name: getProgramName(repository), item_hash: itemHash, entrypoint }); - } catch (error) { - return res.status(400).end('Bad request'); - } + try { + if (req.method !== 'POST') throw new Error('Method not allowed'); + const body = postSchema.parse(req.body); + const {repository, entrypoint} = body; + const path = await clone(repository); + const itemHash = await deploy(path, entrypoint); + return res.status(200).json({name: getProgramName(repository), item_hash: itemHash, entrypoint}); + } catch (error) { + return res.status(400).end('Bad request'); + } } diff --git a/src/components/dashboardPage/FileOptions.tsx b/src/components/dashboardPage/FileOptions.tsx index 777b93e2..a87c20ff 100644 --- a/src/components/dashboardPage/FileOptions.tsx +++ b/src/components/dashboardPage/FileOptions.tsx @@ -1,18 +1,18 @@ import { - Box, - Drawer, - DrawerBody, - DrawerContent, - DrawerOverlay, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, - Portal, - useDisclosure, - VStack, + Box, + Drawer, + DrawerBody, + DrawerContent, + DrawerOverlay, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Portal, + useDisclosure, + VStack, } from '@chakra-ui/react'; -import { useEffect } from 'react'; +import {useEffect} from 'react'; import DeleteFile from 'components/file/DeleteFile'; import DetailsFile from 'components/file/detailsFile/DetailsFile'; @@ -23,106 +23,108 @@ import RestoreFile from 'components/file/RestoreFile'; import ShareFile from 'components/file/ShareFile'; import UpdateContentFile from 'components/file/UpdateContentFile'; -import { IPCFile } from 'types/types'; +import {IPCFile} from 'types/types'; -import { useConfigContext } from 'contexts/config'; +import {useConfigContext} from 'contexts/config'; +import SendEmailFile from "../file/SendEmailFile"; const FileOptionsContent = ({ - file, - files, - onClose, -}: { - file: IPCFile; - files: IPCFile[]; - onClose: () => void; + file, + files, + onClose, + }: { + file: IPCFile; + files: IPCFile[]; + onClose: () => void; }): JSX.Element => ( - <> - {file.deletedAt ? ( - - ) : ( - <> - - - - - - - )} - - - + <> + {file.deletedAt ? ( + + ) : ( + <> + + + + + + + + )} + + + ); const FileOptionsPopover = ({ - file, - files, - clickPosition, - popoverOpeningToggle, - popoverOpeningHandler, -}: { - file: IPCFile; - files: IPCFile[]; - clickPosition: { x: number; y: number }; - popoverOpeningToggle: boolean; - popoverOpeningHandler: () => void; + file, + files, + clickPosition, + popoverOpeningToggle, + popoverOpeningHandler, + }: { + file: IPCFile; + files: IPCFile[]; + clickPosition: { x: number; y: number }; + popoverOpeningToggle: boolean; + popoverOpeningHandler: () => void; }): JSX.Element => { - const { config } = useConfigContext(); - const { isOpen, onOpen, onClose } = useDisclosure(); + const {config} = useConfigContext(); + const {isOpen, onOpen, onClose} = useDisclosure(); - useEffect(() => { - if (popoverOpeningToggle) { - popoverOpeningHandler(); - onOpen(); - } - }, [popoverOpeningToggle]); + useEffect(() => { + if (popoverOpeningToggle) { + popoverOpeningHandler(); + onOpen(); + } + }, [popoverOpeningToggle]); - return ( - onClose()}> - - - - - - - - - - - - - - ); + return ( + onClose()}> + + + + + + + + + + + + + + ); }; const FileOptionsDrawer = ({ - file, - files, - isOpen, - onClose, -}: { - file: IPCFile; - files: IPCFile[]; - isOpen: boolean; - onClose: () => void; + file, + files, + isOpen, + onClose, + }: { + file: IPCFile; + files: IPCFile[]; + isOpen: boolean; + onClose: () => void; }): JSX.Element => ( - - - - - - - - - - + + + + + + + + + + ); -export { FileOptionsPopover, FileOptionsDrawer }; +export {FileOptionsPopover, FileOptionsDrawer}; diff --git a/src/components/file/RenameFile.tsx b/src/components/file/RenameFile.tsx index fd6b2276..22a0154e 100644 --- a/src/components/file/RenameFile.tsx +++ b/src/components/file/RenameFile.tsx @@ -1,15 +1,15 @@ import { - FormControl, - FormLabel, - HStack, - Icon, - Input, - Text, - useBreakpointValue, - useColorMode, - useColorModeValue, - useDisclosure, - useToast, + FormControl, + FormLabel, + HStack, + Icon, + Input, + Text, + useBreakpointValue, + useColorMode, + useColorModeValue, + useDisclosure, + useToast, } from '@chakra-ui/react'; import { ChangeEvent, useState } from 'react'; @@ -40,6 +40,7 @@ const RenameFile = ({ file, concernedFiles, onClosePopover }: RenameFileProps): const isDrawer = useBreakpointValue({ base: true, sm: false }) || false; const toast = useToast({ duration: 2000, isClosable: true }); + const renameFile = async () => { setIsLoading(true); if (name) { @@ -72,13 +73,13 @@ const RenameFile = ({ file, concernedFiles, onClosePopover }: RenameFileProps): return ( OK @@ -119,13 +120,13 @@ const RenameFile = ({ file, concernedFiles, onClosePopover }: RenameFileProps): New file name ) => setName(e.target.value)} - id="ipc-dashboard-input-update-filename" + id='ipc-dashboard-input-update-filename' /> diff --git a/src/components/file/SendEmailFile.tsx b/src/components/file/SendEmailFile.tsx new file mode 100644 index 00000000..a606ab7d --- /dev/null +++ b/src/components/file/SendEmailFile.tsx @@ -0,0 +1,206 @@ +import { + FormControl, + FormErrorMessage, + FormLabel, + HStack, + Icon, + Input, + Text, + Textarea, + useBreakpointValue, + useColorMode, + useColorModeValue, + useDisclosure, + useToast, +} from '@chakra-ui/react'; +import { BiSend } from 'react-icons/bi'; +import React, { useState } from 'react'; +import { IPCFile } from '../../types/types'; +import Button from '../Button'; +import Modal from '../Modal'; +import { textColorMode } from '../../config/colorMode'; +import sendContactForm from '../../lib/email'; + +type SendEmailFileProps = { + file: IPCFile; + onClosePopover: () => void; +}; + +type EmailElements = { + email: string; + subject: string; + message: string; +} + +const initValues: EmailElements = { email: '', subject: '', message: '' }; + + +const initState = { isLoading: false, error: '', values: initValues }; + +const SendEmailFile = ({ onClosePopover }: SendEmailFileProps): JSX.Element => { + const isDrawer = useBreakpointValue({ base: true, sm: false }) || false; + const { isOpen, onOpen, onClose } = useDisclosure(); + const { colorMode } = useColorMode(); + const textColor = useColorModeValue(textColorMode.light, textColorMode.dark); + const [state, setState] = useState(initState); + const toast = useToast({ duration: 2000, isClosable: true }); + const [touched, setTouched] = useState<{ email: boolean, subject: boolean, message: boolean }>({ + email: false, + subject: false, + message: false, + }); + + const { isLoading, error, values } = state; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const onBlur = ({ target }) => + setTouched((prev) => ({ ...prev, [target.name]: true })); + + const sendEmail = async () => { + setState((prev) => ({ + ...prev, + isLoading: true, + })); + try { + await sendContactForm(values); + setTouched({ email: false, subject: false, message: false }); + setState(initState); + toast({ + title: 'Message sent.', + status: 'success', + duration: 2000, + position: 'top', + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + } catch (err: Error) { + setState((prev) => ({ + ...prev, + isLoading: false, + err: err.message, + })); + } + if (values.email) { + onClose(); + onClosePopover(); + } + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const handleChange = ({ target }) => + setState((prev) => ({ + ...prev, + values: { + ...prev.values, + [target.name]: target.value, + }, + })); + + return ( + + + + Send with email + + + Send + + } + > + <> + {error && ( + + {error} + + )} + + To + + Required + + + Subject + + + + + Message +