mailer implemented

This commit is contained in:
Owen Rees
2023-06-21 14:11:16 +02:00
parent 5fd8b35762
commit b323d07d12
11 changed files with 220 additions and 56 deletions
+2 -2
View File
@@ -6,14 +6,13 @@ TESTS
----------------------------------------------------------------- -----------------------------------------------------------------
BUGS 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 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 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 PAGES
//TODO about page //TODO about page
//TODO contact page needs working mailer
---------------------------------------------------------------- ----------------------------------------------------------------
DESIGN CHANGES DESIGN CHANGES
@@ -31,6 +30,7 @@ LOGIC
MISC MISC
---------------------------------------------------------------- ----------------------------------------------------------------
//TODO write logger (fullstack open examples)
//TODO SEO - next headers etc //TODO SEO - next headers etc
//TODO multi-language i18n support - https://nextjs.org/docs/app/building-your-application/routing/internationalization //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 //TODO move smaller ui components into a new folder, and make them reusable - such as using generic prop names
+34
View File
@@ -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: `<p>${message}</p>`,
};
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 });
}
}
+2 -52
View File
@@ -1,4 +1,5 @@
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import ContactForm from "@/components/ContactForm";
// TODO fix page sizing // TODO fix page sizing
export default function Contact() { export default function Contact() {
@@ -17,58 +18,7 @@ export default function Contact() {
Vous avez un problème technique? Vous aimeriez participer à ce Vous avez un problème technique? Vous aimeriez participer à ce
projet? Contactez-nous. projet? Contactez-nous.
</p> </p>
<form action="#" className="space-y-8"> <ContactForm />
<div>
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Adresse e-mail
</label>
<input
type="email"
id="email"
className="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light"
placeholder="nom@exemple.com"
required
/>
</div>
<div>
<label
htmlFor="subject"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Sujet
</label>
<input
type="text"
id="subject"
className="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light"
placeholder="Le motif de ma demande"
required
/>
</div>
<div className="sm:col-span-2">
<label
htmlFor="message"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Votre message
</label>
<textarea
id="message"
rows={6}
className="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg shadow-sm border border-gray-300 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Détaillez ici votre demande..."
></textarea>
</div>
<button
type="submit"
className="py-3 px-5 text-sm font-medium text-center text-white rounded-lg bg-teal-600 sm:w-fit hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 dark:hover:bg-primary-700 dark:focus:ring-primary-800 dark:text-white"
>
Send message
</button>
</form>
</div> </div>
</section> </section>
</Layout> </Layout>
+81
View File
@@ -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 (
<form
onSubmit={(e) => handleEmailSubmit(e, values, setResponseMessage)}
className="space-y-8"
>
<div>
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Adresse e-mail
</label>
<input
value={values.email}
onChange={handleChange}
type="email"
id="email"
className="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light"
placeholder="nom@exemple.com"
required
/>
</div>
<div>
<label
htmlFor="subject"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Sujet
</label>
<input
value={values.subject}
onChange={handleChange}
type="text"
id="subject"
className="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light"
placeholder="Le motif de ma demande"
required
/>
</div>
<div className="sm:col-span-2">
<label
htmlFor="message"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Votre message
</label>
<textarea
value={values.message}
onChange={handleChange}
id="message"
rows={6}
className="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg shadow-sm border border-gray-300 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Détaillez ici votre demande..."
></textarea>
</div>
<button
type="submit"
className="py-3 px-5 text-sm font-medium text-center text-white rounded-lg bg-teal-600 sm:w-fit hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 dark:hover:bg-primary-700 dark:focus:ring-primary-800 dark:text-white"
>
Send message
</button>
</form>
);
};
export default ContactForm;
+2 -2
View File
@@ -3,13 +3,13 @@ import ScrollToTopButton from "@/components/ScrollToTopButton";
describe("Scroll to top button", () => { describe("Scroll to top button", () => {
it("desktop scroll button clickable", () => { it("desktop scroll button clickable", () => {
cy.viewport("macbook-15"); cy.viewport("macbook-15");
cy.mount(<ScrollToTopButton isLgScreen={true} />); cy.mount(<ScrollToTopButton />);
cy.get("[data-cy='scroll-to-top-button']").should("exist").click(); cy.get("[data-cy='scroll-to-top-button']").should("exist").click();
}); });
it("mobile scroll button clickable", () => { it("mobile scroll button clickable", () => {
cy.viewport(600, 600); cy.viewport(600, 600);
cy.mount(<ScrollToTopButton isLgScreen={false} />); cy.mount(<ScrollToTopButton />);
cy.get("[data-cy='scroll-to-top-button']").should("exist").click(); cy.get("[data-cy='scroll-to-top-button']").should("exist").click();
}); });
}); });
+27
View File
@@ -0,0 +1,27 @@
import sendMail from "@/lib/sendMail";
import { Dispatch, FormEvent, SetStateAction } from "react";
export const handleEmailSubmit = async (
e: FormEvent<HTMLFormElement>,
values: Record<string, string>,
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.",
});
}
};
+23
View File
@@ -0,0 +1,23 @@
import { ChangeEvent, useState } from "react";
const useContactForm = () => {
const [values, setValues] = useState({
email: "",
subject: "",
message: "",
});
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setValues((prevState) => {
return {
...prevState,
[e.target.id]: e.target.value,
};
});
};
return { values, handleChange };
};
export default useContactForm;
+2
View File
@@ -1,3 +1,5 @@
// TODO is this really a hook? I think it is more of a util function
interface HamburgerClose { interface HamburgerClose {
menuVisible: boolean; menuVisible: boolean;
setMenuVisible: Dispatch<SetStateAction<boolean>>; setMenuVisible: Dispatch<SetStateAction<boolean>>;
+22
View File
@@ -0,0 +1,22 @@
const sendMail = async ({
email,
subject,
message,
}: Record<string, string>) => {
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;
+23
View File
@@ -19,6 +19,7 @@
"leaflet-defaulticon-compatibility": "^0.1.1", "leaflet-defaulticon-compatibility": "^0.1.1",
"mongodb": "^5.5.0", "mongodb": "^5.5.0",
"next": "^13.4.5", "next": "^13.4.5",
"nodemailer": "^6.9.3",
"postcss": "8.4.23", "postcss": "8.4.23",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -32,10 +33,15 @@
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/nodemailer": "^6.4.8",
"cypress": "^12.13.0", "cypress": "^12.13.0",
"eslint-plugin-cypress": "^2.13.3", "eslint-plugin-cypress": "^2.13.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0" "jest-environment-jsdom": "^29.5.0"
},
"engines": {
"node": ">=18.16.0",
"npm": "please-use-npm"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@@ -1810,6 +1816,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz",
"integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==" "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": { "node_modules/@types/prettier": {
"version": "2.7.2", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", "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", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.11.tgz",
"integrity": "sha512-+M0PwXeU80kRohZ3aT4J/OnR+l9/KD2nVLNNoRgFtnf+umQVFdGBAO2N8+nCnEi0xlh/Wk3zOGC+vNNx+uM79Q==" "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": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+2
View File
@@ -28,6 +28,7 @@
"leaflet-defaulticon-compatibility": "^0.1.1", "leaflet-defaulticon-compatibility": "^0.1.1",
"mongodb": "^5.5.0", "mongodb": "^5.5.0",
"next": "^13.4.5", "next": "^13.4.5",
"nodemailer": "^6.9.3",
"postcss": "8.4.23", "postcss": "8.4.23",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -41,6 +42,7 @@
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/nodemailer": "^6.4.8",
"cypress": "^12.13.0", "cypress": "^12.13.0",
"eslint-plugin-cypress": "^2.13.3", "eslint-plugin-cypress": "^2.13.3",
"jest": "^29.5.0", "jest": "^29.5.0",