diff --git a/TODO b/TODO
index 7af26f3..6724f3e 100644
--- a/TODO
+++ b/TODO
@@ -1,18 +1,42 @@
+TESTS
// TODO tests for tournament page:
- map and table mounts in tournament page <- get data to send to map/table
-----------------------------------------------------------------
-//TODO SRP for web and API data fetching
-//TODO about page
-//TODO contact page needs working mailer
-//TODO font size on mobile screen
-//TODO SEO - next headers etc
//TODO data fetching tests
//TODO redo layer groups tests
+//TODO write tests for contact form
+//TODO dead link tests take too long, split them into separate tests per page
+
+-----------------------------------------------------------------
+BUGS
+//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
+
+----------------------------------------------------------------
+DESIGN CHANGES
+//TODO test entire site for english to french translations
+//TODO success and error messages into french - mailer
+//TODO disable send message button while sending - isSending = true
+//TODO font size on mobile screen
+//TODO bottom of map is a few pixels short
+//TODO mobile navbar is creeping into the page by a few pixels when hidden - move it to the right a bit. It is easier to see in light mode
//TODO logo for navbar and favicon
//TODO mobile map needs improving
-//TODO multi-language i18n support - https://nextjs.org/docs/app/building-your-application/routing/internationalization
+
+----------------------------------------------------------------
+LOGIC
+//TODO SRP for web and API data fetching
//TODO error handling
//TODO consider offering GraphQL support
+
+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
//TODO readme
diff --git a/app/api/send-mail/route.ts b/app/api/send-mail/route.ts
new file mode 100644
index 0000000..882df1c
--- /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} from ${email}`,
+ 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/api/v1/tournaments/france/route.ts b/app/api/v1/tournaments/france/route.ts
index 9b11c04..afcd444 100644
--- a/app/api/v1/tournaments/france/route.ts
+++ b/app/api/v1/tournaments/france/route.ts
@@ -16,7 +16,7 @@ export async function GET() {
const db = client.db("tournamentsFranceDB");
const results = await dateOrderingFrance(db);
- const data = results.map(({ _id, __v, ...rest }) => ({
+ const data = results.map(({ _id, ...rest }) => ({
id: _id,
...rest,
}));
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..d80148a
--- /dev/null
+++ b/components/ContactForm.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { useState } from "react";
+import { handleEmailSubmit } from "@/handlers/formSubmitHandlers";
+import useContactForm from "@/hooks/useContactForm";
+
+const ContactForm = () => {
+ const { values, handleChange, resetForm } = useContactForm();
+ const [responseMessage, setResponseMessage] = useState({
+ isSuccessful: false,
+ message: "",
+ });
+ const [isSending, setIsSending] = useState(false);
+
+ const infoMessage = (
+
+ {responseMessage.message}
+
+ );
+
+ return (
+ <>
+
+ handleEmailSubmit(
+ e,
+ values,
+ setResponseMessage,
+ resetForm,
+ setIsSending
+ )
+ }
+ className="space-y-8"
+ >
+
+
+ Adresse e-mail
+
+
+
+
+
+ Sujet
+
+
+
+
+
+ Votre message
+
+
+
+
+ {isSending ? "Sending..." : "Send Message"}
+
+ {infoMessage}
+
+ >
+ );
+};
+
+export default ContactForm;
diff --git a/components/ScrollToTopButton.tsx b/components/ScrollToTopButton.tsx
index 528492a..ba63e6a 100644
--- a/components/ScrollToTopButton.tsx
+++ b/components/ScrollToTopButton.tsx
@@ -1,17 +1,32 @@
"use client";
+import { ScrollableElement } from "@/types";
+
import { FaArrowUp } from "react-icons/fa";
import { handleScrollToTop } from "@/handlers/scrollHandlers";
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
+
+const ScrollToTopButton = () => {
+ const scrollToTopElementRef = useRef(null);
+ const [isLgScreen, setIsLgScreen] = useState(false);
+
+ // calculate screen size
+ useEffect(() => {
+ const handleResize = () => {
+ setIsLgScreen(window.innerWidth >= 1024);
+ };
+ handleResize();
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ });
-const ScrollToTopButton = ({ isLgScreen }: { isLgScreen: boolean }) => {
- const scrollToTopElementRef = useRef(null);
// determine scrollable element based on screen size - window or div
useEffect(() => {
- if (isLgScreen) {
- scrollToTopElementRef.current =
- document.getElementById("tournament-table");
- }
+ isLgScreen
+ ? (scrollToTopElementRef.current =
+ document.getElementById("tournament-table"))
+ : (scrollToTopElementRef.current = window);
}, [isLgScreen]);
const scrollToTopButtonClass = isLgScreen
@@ -24,9 +39,7 @@ const ScrollToTopButton = ({ isLgScreen }: { isLgScreen: boolean }) => {
data-cy="scroll-to-top-button"
>
- handleScrollToTop(scrollToTopElementRef.current || window)
- }
+ onClick={() => handleScrollToTop(scrollToTopElementRef.current)}
/>
);
diff --git a/components/TournamentMap.tsx b/components/TournamentMap.tsx
index e8a420a..0323030 100644
--- a/components/TournamentMap.tsx
+++ b/components/TournamentMap.tsx
@@ -6,7 +6,7 @@ import { LatLngLiteral } from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";
-import { MapContainer, TileLayer, LayersControl, useMap } from "react-leaflet";
+import { MapContainer, TileLayer, LayersControl } from "react-leaflet";
import { createLayerGroups } from "@/utils/layerGroups";
import Legend from "@/components/Legend";
diff --git a/components/TournamentTable.tsx b/components/TournamentTable.tsx
index 4740b75..ace2453 100644
--- a/components/TournamentTable.tsx
+++ b/components/TournamentTable.tsx
@@ -12,7 +12,6 @@ export default function TournamentTable({
const [searchQuery, setSearchQuery] = useState(""); // text from search bar
const [filteredTournamentData, setFilteredTournamentData] =
useState(tournamentData);
- const [isLgScreen, setIsLgScreen] = useState(false);
useEffect(() => {
setFilteredTournamentData(
@@ -20,18 +19,6 @@ export default function TournamentTable({
);
}, [searchQuery]);
- useEffect(() => {
- const handleResize = () => {
- setIsLgScreen(window.innerWidth >= 1024);
- };
- handleResize();
-
- window.addEventListener("resize", handleResize);
- return () => {
- window.removeEventListener("resize", handleResize);
- };
- }, []);
-
// TODO move this section into its own function
if (filteredTournamentData.length === 0) {
tableData = (
@@ -83,7 +70,7 @@ export default function TournamentTable({
setTournamentFilter={setSearchQuery}
/>
-
+
{
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/cypress/screenshots/links.cy.ts/Test all links -- Check navbar links point to correct pathname as a slug (failed).png b/cypress/screenshots/links.cy.ts/Test all links -- Check navbar links point to correct pathname as a slug (failed).png
new file mode 100644
index 0000000..b7a9591
Binary files /dev/null and b/cypress/screenshots/links.cy.ts/Test all links -- Check navbar links point to correct pathname as a slug (failed).png differ
diff --git a/cypress/videos/data.cy.tsx.mp4 b/cypress/videos/data.cy.tsx.mp4
index da8a0bd..7d5e483 100644
Binary files a/cypress/videos/data.cy.tsx.mp4 and b/cypress/videos/data.cy.tsx.mp4 differ
diff --git a/cypress/videos/links.cy.ts.mp4 b/cypress/videos/links.cy.ts.mp4
index 6702f32..85a1e19 100644
Binary files a/cypress/videos/links.cy.ts.mp4 and b/cypress/videos/links.cy.ts.mp4 differ
diff --git a/cypress/videos/navbar.cy.tsx.mp4 b/cypress/videos/navbar.cy.tsx.mp4
index 9c8c0e7..63f71ca 100644
Binary files a/cypress/videos/navbar.cy.tsx.mp4 and b/cypress/videos/navbar.cy.tsx.mp4 differ
diff --git a/cypress/videos/scroll.cy.tsx.mp4 b/cypress/videos/scroll.cy.tsx.mp4
index 84b62b8..e82e785 100644
Binary files a/cypress/videos/scroll.cy.tsx.mp4 and b/cypress/videos/scroll.cy.tsx.mp4 differ
diff --git a/handlers/formSubmitHandlers.ts b/handlers/formSubmitHandlers.ts
new file mode 100644
index 0000000..8a4692e
--- /dev/null
+++ b/handlers/formSubmitHandlers.ts
@@ -0,0 +1,45 @@
+import { Dispatch, FormEvent, SetStateAction } from "react";
+import sendMail from "@/lib/sendMail";
+
+export const handleEmailSubmit = async (
+ e: FormEvent,
+ values: Record,
+ setResponseMessage: Dispatch<
+ SetStateAction<{ isSuccessful: boolean; message: string }>
+ >,
+ resetForm: () => void,
+ setIsSending: Dispatch>
+) => {
+ e.preventDefault();
+ setIsSending(true);
+
+ const clearMessage = () => {
+ setTimeout(() => {
+ setResponseMessage({
+ isSuccessful: false,
+ message: "",
+ });
+ }, 10000);
+ };
+
+ try {
+ const response = await sendMail(values);
+ if (response.status === 250) {
+ setResponseMessage({
+ isSuccessful: true,
+ message: "Thank you for your message.",
+ });
+ resetForm();
+ clearMessage();
+ setIsSending(false);
+ }
+ } catch (error) {
+ console.log(error); //TODO add to logger
+ setResponseMessage({
+ isSuccessful: false,
+ message: "Oops something went wrong. Please try again.",
+ });
+ clearMessage();
+ setIsSending(false);
+ }
+};
diff --git a/hooks/useContactForm.ts b/hooks/useContactForm.ts
new file mode 100644
index 0000000..f917a5c
--- /dev/null
+++ b/hooks/useContactForm.ts
@@ -0,0 +1,28 @@
+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,
+ };
+ });
+ };
+
+ const resetForm = () => {
+ setValues({ email: "", subject: "", message: "" });
+ };
+
+ return { values, handleChange, resetForm };
+};
+
+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",