From 844e3cb8756495bd6987994cf18a63eeefc005c7 Mon Sep 17 00:00:00 2001 From: DrMint <thomas@barillot.net> Date: Wed, 23 Mar 2022 16:19:23 +0100 Subject: [PATCH 1/2] Added contact system --- README.md | 3 + package-lock.json | 45 +++++- package.json | 2 + src/graphql/operation.graphql | 7 + src/graphql/operations-types.ts | 7 + src/pages/about-us/contact.tsx | 225 ++++++++++++++++++++++++++ src/pages/about-us/sharing-policy.tsx | 2 +- src/pages/api/mail.ts | 51 ++++++ src/pages/editor.tsx | 2 +- src/queries/helpers.ts | 4 + src/tailwind.css | 18 ++- 11 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 src/pages/about-us/contact.tsx create mode 100644 src/pages/api/mail.ts diff --git a/README.md b/README.md index 7912e64..a37b4b6 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ Enter the followind information: ```txt URL_GRAPHQL=https://url-to.strapi-accords-library.com/graphql ACCESS_TOKEN=genatedcode-by-strapi-api +SMTP_HOST=email.provider.com +SMTP_USER=email@example.com +SMTP_PASSWORD=mypassword123 NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com/ NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com/ NEXT_PUBLIC_URL_SELF=https://url-to-front-accords-library.com diff --git a/package-lock.json b/package-lock.json index a6f4ebf..d38fcc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@tippyjs/react": "^4.2.6", "markdown-to-jsx": "^7.1.7", "next": "^12.1.0", + "nodemailer": "^6.7.3", "react": "17.0.2", "react-dom": "17.0.2", "react-image-lightbox": "^5.1.4", @@ -22,6 +23,7 @@ }, "devDependencies": { "@types/node": "17.0.21", + "@types/nodemailer": "^6.4.4", "@types/react": "17.0.40", "@types/react-dom": "^17.0.13", "eslint": "8.10.0", @@ -489,6 +491,15 @@ "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==", "dev": true }, + "node_modules/@types/nodemailer": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", + "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -2493,9 +2504,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/ms": { @@ -2594,6 +2605,14 @@ "dev": true, "peer": true }, + "node_modules/nodemailer": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.3.tgz", + "integrity": "sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g==", + "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", @@ -4004,6 +4023,15 @@ "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==", "dev": true }, + "@types/nodemailer": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", + "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -5485,9 +5513,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "ms": { @@ -5549,6 +5577,11 @@ "dev": true, "peer": true }, + "nodemailer": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.3.tgz", + "integrity": "sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g==" + }, "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 69a7da8..9b9738f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tippyjs/react": "^4.2.6", "markdown-to-jsx": "^7.1.7", "next": "^12.1.0", + "nodemailer": "^6.7.3", "react": "17.0.2", "react-dom": "17.0.2", "react-image-lightbox": "^5.1.4", @@ -24,6 +25,7 @@ }, "devDependencies": { "@types/node": "17.0.21", + "@types/nodemailer": "^6.4.4", "@types/react": "17.0.40", "@types/react-dom": "^17.0.13", "eslint": "8.10.0", diff --git a/src/graphql/operation.graphql b/src/graphql/operation.graphql index e4fa3f8..f4353e7 100644 --- a/src/graphql/operation.graphql +++ b/src/graphql/operation.graphql @@ -125,6 +125,13 @@ query getWebsiteInterface($language_code: String) { members sharing_policy contact_us + email + email_gdpr_notice + message + send + response_invalid_code + response_invalid_email + response_email_success } } } diff --git a/src/graphql/operations-types.ts b/src/graphql/operations-types.ts index 13ff3ef..9a5259a 100644 --- a/src/graphql/operations-types.ts +++ b/src/graphql/operations-types.ts @@ -214,6 +214,13 @@ export type GetWebsiteInterfaceQuery = { members: string; sharing_policy: string; contact_us: string; + email: string; + email_gdpr_notice: string; + message: string; + send: string; + response_invalid_code: string; + response_invalid_email: string; + response_email_success: string; }; }>; }; diff --git a/src/pages/about-us/contact.tsx b/src/pages/about-us/contact.tsx new file mode 100644 index 0000000..1121dba --- /dev/null +++ b/src/pages/about-us/contact.tsx @@ -0,0 +1,225 @@ +import SubPanel from "components/Panels/SubPanel"; +import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; +import ReturnButton, { + ReturnButtonType, +} from "components/PanelComponents/ReturnButton"; +import AppLayout from "components/AppLayout"; +import ContentPanel from "components/Panels/ContentPanel"; +import { GetStaticProps } from "next"; +import { getPost, getPostLanguages } from "graphql/operations"; +import { GetPostQuery } from "graphql/operations-types"; +import { useRouter } from "next/router"; +import LanguageSwitcher from "components/LanguageSwitcher"; +import Markdawn from "components/Markdown/Markdawn"; +import { RequestMailProps, ResponseMailProps } from "pages/api/mail"; +import { useState } from "react"; +import InsetBox from "components/InsetBox"; +import { randomInt } from "queries/helpers"; +import TOC from "components/Markdown/TOC"; + +interface ContactProps extends AppStaticProps { + post: GetPostQuery["posts"]["data"][number]["attributes"]; + locales: string[]; +} + +export default function AboutUs(props: ContactProps): JSX.Element { + const { langui, post, locales } = props; + const router = useRouter(); + const [formResponse, setFormResponse] = useState(""); + const [formCompleted, setFormCompleted] = useState(false); + + const random1 = randomInt(0, 10); + const random2 = randomInt(0, 10); + + const subPanel = ( + <SubPanel> + <ReturnButton + href="/about-us" + displayOn={ReturnButtonType.Desktop} + langui={langui} + title={langui.about_us} + horizontalLine + /> + {post.translations.length > 0 && post.translations[0].body && ( + <TOC + text={post.translations[0].body} + router={router} + title={post.translations[0].title} + /> + )} + </SubPanel> + ); + + const contentPanel = ( + <ContentPanel> + <ReturnButton + href="/about-us" + displayOn={ReturnButtonType.Mobile} + langui={langui} + title={langui.about_us} + className="mb-10" + /> + {locales.includes(router.locale || "en") ? ( + <Markdawn router={router} text={post.translations[0].body} /> + ) : ( + <LanguageSwitcher + locales={locales} + router={router} + languages={props.languages} + langui={props.langui} + /> + )} + + <div className="flex flex-col gap-8 text-center"> + <form + className={`gap-8 grid ${ + formCompleted && + "opacity-60 cursor-not-allowed touch-none pointer-events-none" + }`} + onSubmit={(e) => { + e.preventDefault(); + + if (e.target.verif.value == random1 + random2 && !formCompleted) { + const content: RequestMailProps = { + name: e.target.name.value, + email: e.target.email.value, + message: e.target.message.value, + formName: "Contact Form", + }; + fetch("/api/mail", { + method: "POST", + body: JSON.stringify(content), + headers: { + "Content-type": "application/json; charset=UTF-8", + }, + }) + .then((response) => response.json()) + .then((data: ResponseMailProps) => { + switch (data.code) { + case "OKAY": + setFormResponse(langui.response_email_success); + setFormCompleted(true); + break; + + case "EENVELOPE": + langui.response_invalid_email; + break; + + default: + setFormResponse(data.message || ""); + break; + } + }); + } else { + setFormResponse(langui.response_invalid_code); + } + + router.replace("#send-response"); + e.target.verif.value = ""; + }} + > + <div className="flex flex-col place-items-center gap-1"> + <label htmlFor="name">{langui.name}:</label> + <input + type="text" + className="mobile:w-full" + name="name" + id="name" + required + disabled={formCompleted} + /> + </div> + + <div className="flex flex-col place-items-center gap-1"> + <label htmlFor="email">{langui.email}:</label> + <input + type="email" + className="mobile:w-full" + name="email" + id="email" + required + disabled={formCompleted} + /> + <p className="text-sm text-dark italic opacity-70"> + {langui.email_gdpr_notice} + </p> + </div> + + <div className="flex flex-col place-items-center gap-1 w-full"> + <label htmlFor="message">{langui.message}:</label> + <textarea + name="message" + id="message" + className="w-full" + rows={8} + required + disabled={formCompleted} + /> + </div> + + <div className="grid grid-cols-2 place-items-center"> + <div className="flex flex-row place-items-center gap-2"> + <label + className="flex-shrink-0" + htmlFor="verif" + >{`${random1} + ${random2} =`}</label> + <input + className="w-24" + type="number" + name="verif" + id="verif" + required + disabled={formCompleted} + /> + </div> + + <input + type="submit" + value={langui.send} + className="w-min !px-6" + disabled={formCompleted} + /> + </div> + </form> + + <div id="send-response"> + {formResponse && ( + <InsetBox> + <p>{formResponse}</p> + </InsetBox> + )} + </div> + </div> + </ContentPanel> + ); + + return ( + <AppLayout + navTitle={"Contact"} + subPanel={subPanel} + contentPanel={contentPanel} + {...props} + /> + ); +} + +export const getStaticProps: GetStaticProps = async (context) => { + const slug = "contact"; + const props: ContactProps = { + ...(await getAppStaticProps(context)), + post: ( + await getPost({ + slug: slug, + language_code: context.locale || "en", + }) + ).posts.data[0].attributes, + locales: ( + await getPostLanguages({ slug: slug }) + ).posts.data[0].attributes.translations.map((translation) => { + return translation.language.data.attributes.code; + }), + }; + return { + props: props, + }; +}; diff --git a/src/pages/about-us/sharing-policy.tsx b/src/pages/about-us/sharing-policy.tsx index f0a9a25..90e045d 100644 --- a/src/pages/about-us/sharing-policy.tsx +++ b/src/pages/about-us/sharing-policy.tsx @@ -84,7 +84,7 @@ export const getStaticProps: GetStaticProps = async (context) => { ...(await getAppStaticProps(context)), post: ( await getPost({ - slug: "sharing-policy", + slug: slug, language_code: context.locale || "en", }) ).posts.data[0].attributes, diff --git a/src/pages/api/mail.ts b/src/pages/api/mail.ts new file mode 100644 index 0000000..c7e38c1 --- /dev/null +++ b/src/pages/api/mail.ts @@ -0,0 +1,51 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import nodemailer from "nodemailer"; +import { SMTPError } from "nodemailer/lib/smtp-connection"; + +export type ResponseMailProps = { + code?: string; + message?: string; +}; + +export type RequestMailProps = { + name: string; + email: string; + message: string; + formName: string; +}; + +export default async function Mail( + req: NextApiRequest, + res: NextApiResponse<ResponseMailProps> +) { + if (req.method === "POST") { + const body = req.body as RequestMailProps; + + let transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, + }); + + // send mail with defined transport object + let info = await transporter + .sendMail({ + from: `"${body.name}" <${body.email}>`, + to: "contact@accords-library.com", + subject: `New ${body.formName} from ${body.name}`, + text: body.message, + }) + .catch((reason: SMTPError) => { + res.status(reason.responseCode || 500).json({ + code: reason.code, + message: reason.response, + }); + }); + } + + res.status(200).json({ code: "OKAY" }); +} diff --git a/src/pages/editor.tsx b/src/pages/editor.tsx index b770c0e..9859fe5 100644 --- a/src/pages/editor.tsx +++ b/src/pages/editor.tsx @@ -72,7 +72,7 @@ export default function Editor(props: EditorProps): JSX.Element { target.select(); event.preventDefault(); }} - className="bg-mid rounded-xl p-8 w-full font-monospace" + className="font-monospace" /> </div> <div> diff --git a/src/queries/helpers.ts b/src/queries/helpers.ts index 7393aa1..9b938c5 100644 --- a/src/queries/helpers.ts +++ b/src/queries/helpers.ts @@ -291,3 +291,7 @@ export function slugify(string: string | undefined): string { .replace(/ /gi, "-") .toLowerCase(); } + +export function randomInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min)) + min; +} diff --git a/src/tailwind.css b/src/tailwind.css index 80f03cd..e13be31 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -136,16 +136,26 @@ /* INPUT */ - input { - @apply rounded-full p-2 text-center bg-light outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent] text-dark hover:bg-mid transition-all; + input, + textarea { + @apply rounded-full p-2 text-center bg-light outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent] text-dark hover:bg-mid transition-all placeholder:text-dark placeholder:opacity-60; } input::placeholder { @apply text-dark opacity-60; } - input:focus-visible { - @apply outline-none bg-mid shadow-inner-sm; + input:focus-visible, + textarea:focus-within { + @apply outline-none bg-mid shadow-inner-sm shadow-shade; + } + + textarea { + @apply rounded-2xl text-left p-6; + } + + input[type="submit"] { + @apply grid place-content-center place-items-center border-[1px] border-dark text-dark rounded-full px-4 pt-[0.4rem] pb-[0.5rem] transition-all cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:text-light active:drop-shadow-black-lg active:border-black; } } From b946974267cb5f7ee652577956c671ce35ecea35 Mon Sep 17 00:00:00 2001 From: DrMint <thomas@barillot.net> Date: Wed, 23 Mar 2022 16:45:41 +0100 Subject: [PATCH 2/2] Added more states to the form --- src/pages/about-us/contact.tsx | 46 +++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/pages/about-us/contact.tsx b/src/pages/about-us/contact.tsx index 1121dba..071f133 100644 --- a/src/pages/about-us/contact.tsx +++ b/src/pages/about-us/contact.tsx @@ -26,7 +26,9 @@ export default function AboutUs(props: ContactProps): JSX.Element { const { langui, post, locales } = props; const router = useRouter(); const [formResponse, setFormResponse] = useState(""); - const [formCompleted, setFormCompleted] = useState(false); + const [formState, setFormState] = useState<"stale" | "ongoing" | "completed">( + "stale" + ); const random1 = randomInt(0, 10); const random2 = randomInt(0, 10); @@ -73,17 +75,29 @@ export default function AboutUs(props: ContactProps): JSX.Element { <div className="flex flex-col gap-8 text-center"> <form className={`gap-8 grid ${ - formCompleted && + formState !== "stale" && "opacity-60 cursor-not-allowed touch-none pointer-events-none" }`} onSubmit={(e) => { e.preventDefault(); - if (e.target.verif.value == random1 + random2 && !formCompleted) { + const fields = e.target as unknown as { + verif: HTMLInputElement; + name: HTMLInputElement; + email: HTMLInputElement; + message: HTMLInputElement; + }; + + setFormState("ongoing"); + + if ( + parseInt(fields.verif.value) == random1 + random2 && + formState !== "completed" + ) { const content: RequestMailProps = { - name: e.target.name.value, - email: e.target.email.value, - message: e.target.message.value, + name: fields.name.value, + email: fields.email.value, + message: fields.message.value, formName: "Contact Form", }; fetch("/api/mail", { @@ -98,24 +112,28 @@ export default function AboutUs(props: ContactProps): JSX.Element { switch (data.code) { case "OKAY": setFormResponse(langui.response_email_success); - setFormCompleted(true); + setFormState("completed"); + break; case "EENVELOPE": - langui.response_invalid_email; + setFormResponse(langui.response_invalid_email); + setFormState("stale"); break; default: setFormResponse(data.message || ""); + setFormState("stale"); break; } }); } else { setFormResponse(langui.response_invalid_code); + setFormState("stale"); } router.replace("#send-response"); - e.target.verif.value = ""; + fields.verif.value = ""; }} > <div className="flex flex-col place-items-center gap-1"> @@ -126,7 +144,7 @@ export default function AboutUs(props: ContactProps): JSX.Element { name="name" id="name" required - disabled={formCompleted} + disabled={formState !== "stale"} /> </div> @@ -138,7 +156,7 @@ export default function AboutUs(props: ContactProps): JSX.Element { name="email" id="email" required - disabled={formCompleted} + disabled={formState !== "stale"} /> <p className="text-sm text-dark italic opacity-70"> {langui.email_gdpr_notice} @@ -153,7 +171,7 @@ export default function AboutUs(props: ContactProps): JSX.Element { className="w-full" rows={8} required - disabled={formCompleted} + disabled={formState !== "stale"} /> </div> @@ -169,7 +187,7 @@ export default function AboutUs(props: ContactProps): JSX.Element { name="verif" id="verif" required - disabled={formCompleted} + disabled={formState !== "stale"} /> </div> @@ -177,7 +195,7 @@ export default function AboutUs(props: ContactProps): JSX.Element { type="submit" value={langui.send} className="w-min !px-6" - disabled={formCompleted} + disabled={formState !== "stale"} /> </div> </form>