From b323d07d1212289e745edf5006045342d61c7e91 Mon Sep 17 00:00:00 2001 From: Owen Rees Date: Wed, 21 Jun 2023 14:11:16 +0200 Subject: [PATCH] mailer implemented --- TODO | 4 +- app/api/send-mail/route.ts | 34 ++++++++++++++ app/contactez-nous/page.tsx | 54 +--------------------- components/ContactForm.tsx | 81 +++++++++++++++++++++++++++++++++ cypress/component/scroll.cy.tsx | 4 +- handlers/formSubmitHandlers.ts | 27 +++++++++++ hooks/useContactForm.ts | 23 ++++++++++ hooks/useHamburgerClose.ts | 2 + lib/sendMail.ts | 22 +++++++++ package-lock.json | 23 ++++++++++ package.json | 2 + 11 files changed, 220 insertions(+), 56 deletions(-) create mode 100644 app/api/send-mail/route.ts create mode 100644 components/ContactForm.tsx create mode 100644 handlers/formSubmitHandlers.ts create mode 100644 hooks/useContactForm.ts create mode 100644 lib/sendMail.ts diff --git a/TODO b/TODO index 1c99e3f..06ac2d6 100644 --- a/TODO +++ b/TODO @@ -6,14 +6,13 @@ TESTS ----------------------------------------------------------------- BUGS -//TODO small screen scroll to top button only works after a refresh. The bug appears after a screen resize - TournamentTable.tsx useEffect //TODO about page is not centred in Safari //TODO tournament page load is weird on 2nd click. On safari it pauses for a few seconds. Is it trying to load the entire page before displaying? +//TODO Nodemailer 'from' reverts to echecsfrance@gmail.com ---------------------------------------------------------------- PAGES //TODO about page -//TODO contact page needs working mailer ---------------------------------------------------------------- DESIGN CHANGES @@ -31,6 +30,7 @@ LOGIC MISC ---------------------------------------------------------------- +//TODO write logger (fullstack open examples) //TODO SEO - next headers etc //TODO multi-language i18n support - https://nextjs.org/docs/app/building-your-application/routing/internationalization //TODO move smaller ui components into a new folder, and make them reusable - such as using generic prop names diff --git a/app/api/send-mail/route.ts b/app/api/send-mail/route.ts new file mode 100644 index 0000000..fe16e80 --- /dev/null +++ b/app/api/send-mail/route.ts @@ -0,0 +1,34 @@ +import NodeMailer from "nodemailer"; +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + const { email, subject, message } = await req.json(); + + try { + const mailContent = { + from: email, + to: process.env.GMAIL_USER, + subject: subject, + text: message, + html: `

${message}

`, + }; + + const transporter = NodeMailer.createTransport({ + service: "gmail", + auth: { + user: process.env.GMAIL_USER, + pass: process.env.GMAIL_PASS, + }, + }); + + const info = await transporter.sendMail(mailContent); + console.log(info); + + return NextResponse.json( + { success: `Message delivered to ${info.accepted}` }, + { status: 250 } + ); + } catch (error) { + return NextResponse.json({ error: `Connection refused` }, { status: 404 }); + } +} diff --git a/app/contactez-nous/page.tsx b/app/contactez-nous/page.tsx index 3006137..b2e8bde 100644 --- a/app/contactez-nous/page.tsx +++ b/app/contactez-nous/page.tsx @@ -1,4 +1,5 @@ import Layout from "@/components/Layout"; +import ContactForm from "@/components/ContactForm"; // TODO fix page sizing export default function Contact() { @@ -17,58 +18,7 @@ export default function Contact() { Vous avez un problème technique? Vous aimeriez participer à ce projet? Contactez-nous.

-
-
- - -
-
- - -
-
- - -
- -
+ diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx new file mode 100644 index 0000000..8f3ddbe --- /dev/null +++ b/components/ContactForm.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useState } from "react"; +import { handleEmailSubmit } from "@/handlers/formSubmitHandlers"; +import useContactForm from "@/hooks/useContactForm"; + +const ContactForm = () => { + const { values, handleChange } = useContactForm(); + const [responseMessage, setResponseMessage] = useState({ + isSuccessful: false, + message: "", + }); + + console.log(responseMessage); + + return ( +
handleEmailSubmit(e, values, setResponseMessage)} + className="space-y-8" + > +
+ + +
+
+ + +
+
+ + +
+ +
+ ); +}; + +export default ContactForm; diff --git a/cypress/component/scroll.cy.tsx b/cypress/component/scroll.cy.tsx index f070c18..037a1d0 100644 --- a/cypress/component/scroll.cy.tsx +++ b/cypress/component/scroll.cy.tsx @@ -3,13 +3,13 @@ import ScrollToTopButton from "@/components/ScrollToTopButton"; describe("Scroll to top button", () => { it("desktop scroll button clickable", () => { cy.viewport("macbook-15"); - cy.mount(); + cy.mount(); cy.get("[data-cy='scroll-to-top-button']").should("exist").click(); }); it("mobile scroll button clickable", () => { cy.viewport(600, 600); - cy.mount(); + cy.mount(); cy.get("[data-cy='scroll-to-top-button']").should("exist").click(); }); }); diff --git a/handlers/formSubmitHandlers.ts b/handlers/formSubmitHandlers.ts new file mode 100644 index 0000000..29b9407 --- /dev/null +++ b/handlers/formSubmitHandlers.ts @@ -0,0 +1,27 @@ +import sendMail from "@/lib/sendMail"; +import { Dispatch, FormEvent, SetStateAction } from "react"; + +export const handleEmailSubmit = async ( + e: FormEvent, + values: Record, + setResponseMessage: Dispatch< + SetStateAction<{ isSuccessful: boolean; message: string }> + > +) => { + e.preventDefault(); + try { + const response = await sendMail(values); + if (response.status === 250) { + setResponseMessage({ + isSuccessful: true, + message: "Thank you for your message.", + }); + } + } catch (error) { + console.log(error); + setResponseMessage({ + isSuccessful: false, + message: "Oops something went wrong. Please try again.", + }); + } +}; diff --git a/hooks/useContactForm.ts b/hooks/useContactForm.ts new file mode 100644 index 0000000..54b6128 --- /dev/null +++ b/hooks/useContactForm.ts @@ -0,0 +1,23 @@ +import { ChangeEvent, useState } from "react"; + +const useContactForm = () => { + const [values, setValues] = useState({ + email: "", + subject: "", + message: "", + }); + + const handleChange = ( + e: ChangeEvent + ) => { + setValues((prevState) => { + return { + ...prevState, + [e.target.id]: e.target.value, + }; + }); + }; + return { values, handleChange }; +}; + +export default useContactForm; diff --git a/hooks/useHamburgerClose.ts b/hooks/useHamburgerClose.ts index caed63f..a26e6ae 100644 --- a/hooks/useHamburgerClose.ts +++ b/hooks/useHamburgerClose.ts @@ -1,3 +1,5 @@ +// TODO is this really a hook? I think it is more of a util function + interface HamburgerClose { menuVisible: boolean; setMenuVisible: Dispatch>; diff --git a/lib/sendMail.ts b/lib/sendMail.ts new file mode 100644 index 0000000..5735fe8 --- /dev/null +++ b/lib/sendMail.ts @@ -0,0 +1,22 @@ +const sendMail = async ({ + email, + subject, + message, +}: Record) => { + const data = { + email: email, + subject: subject, + message: message, + }; + + const response = await fetch("/api/send-mail", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + return response; +}; + +export default sendMail; diff --git a/package-lock.json b/package-lock.json index 73b87a4..daf5dfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "leaflet-defaulticon-compatibility": "^0.1.1", "mongodb": "^5.5.0", "next": "^13.4.5", + "nodemailer": "^6.9.3", "postcss": "8.4.23", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -32,10 +33,15 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/leaflet": "^1.9.3", + "@types/nodemailer": "^6.4.8", "cypress": "^12.13.0", "eslint-plugin-cypress": "^2.13.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0" + }, + "engines": { + "node": ">=18.16.0", + "npm": "please-use-npm" } }, "node_modules/@adobe/css-tools": { @@ -1810,6 +1816,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz", "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==" }, + "node_modules/@types/nodemailer": { + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.8.tgz", + "integrity": "sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -7413,6 +7428,14 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.11.tgz", "integrity": "sha512-+M0PwXeU80kRohZ3aT4J/OnR+l9/KD2nVLNNoRgFtnf+umQVFdGBAO2N8+nCnEi0xlh/Wk3zOGC+vNNx+uM79Q==" }, + "node_modules/nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index 3e29ac2..96dbc13 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "leaflet-defaulticon-compatibility": "^0.1.1", "mongodb": "^5.5.0", "next": "^13.4.5", + "nodemailer": "^6.9.3", "postcss": "8.4.23", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -41,6 +42,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/leaflet": "^1.9.3", + "@types/nodemailer": "^6.4.8", "cypress": "^12.13.0", "eslint-plugin-cypress": "^2.13.3", "jest": "^29.5.0",