Added contact system

This commit is contained in:
DrMint 2022-03-23 16:19:23 +01:00
parent 8258475a63
commit 844e3cb875
11 changed files with 354 additions and 12 deletions

View File

@ -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

45
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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
}
}
}

View File

@ -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;
};
}>;
};

View File

@ -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,
};
};

View File

@ -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,

51
src/pages/api/mail.ts Normal file
View File

@ -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" });
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}
}