From d33d447f9038a440361a59bf0e4606b18c7e7f07 Mon Sep 17 00:00:00 2001 From: Owen Rees Date: Tue, 29 Aug 2023 22:55:22 +0200 Subject: [PATCH] Add Tournament form --- app/[lang]/ajouter-un-tournoi/Map.tsx | 112 ++++++++ .../ajouter-un-tournoi/TournamentForm.tsx | 268 ++++++++++++++++++ app/[lang]/ajouter-un-tournoi/page.tsx | 24 ++ app/[lang]/components/InfoMessage.tsx | 30 ++ app/[lang]/components/LoadingMap.tsx | 12 + app/[lang]/components/MapEvents.tsx | 31 ++ app/[lang]/contactez-nous/ContactForm.tsx | 71 ++--- app/[lang]/tournois/TournamentMap.tsx | 43 +-- app/[lang]/tournois/TournamentsDisplay.tsx | 23 +- app/[lang]/tournois/page.tsx | 3 +- app/api/add-tournament/route.ts | 53 ++++ app/atoms.ts | 22 +- css/globals.css | 11 + handlers/formHandlers.ts | 84 ++++++ hooks/useTournamentForm.ts | 42 +++ lib/discordWebhook.ts | 48 ++++ lib/tournamentFormToDB.ts | 28 ++ messages/en.json | 44 +++ messages/fr.json | 45 +++ types.ts | 30 ++ utils/formatDate.ts | 10 + 21 files changed, 924 insertions(+), 110 deletions(-) create mode 100644 app/[lang]/ajouter-un-tournoi/Map.tsx create mode 100644 app/[lang]/ajouter-un-tournoi/TournamentForm.tsx create mode 100644 app/[lang]/ajouter-un-tournoi/page.tsx create mode 100644 app/[lang]/components/InfoMessage.tsx create mode 100644 app/[lang]/components/LoadingMap.tsx create mode 100644 app/[lang]/components/MapEvents.tsx create mode 100644 app/api/add-tournament/route.ts create mode 100644 handlers/formHandlers.ts create mode 100644 hooks/useTournamentForm.ts create mode 100644 lib/discordWebhook.ts create mode 100644 lib/tournamentFormToDB.ts create mode 100644 utils/formatDate.ts diff --git a/app/[lang]/ajouter-un-tournoi/Map.tsx b/app/[lang]/ajouter-un-tournoi/Map.tsx new file mode 100644 index 0000000..bba2e7d --- /dev/null +++ b/app/[lang]/ajouter-un-tournoi/Map.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { MapProps } from "@/types"; +import { useMemo, useRef, ChangeEvent } from "react"; +import { useTranslations } from "next-intl"; +import { useSetAtom } from "jotai"; +import { mapBoundsAtom } from "@/app/atoms"; + +import L from "leaflet"; +import { MapContainer, TileLayer, Marker } from "react-leaflet"; +import "leaflet/dist/leaflet.css"; +import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; +import "leaflet-defaulticon-compatibility"; + +import MapEvents from "@/app/[lang]/components/MapEvents"; + +const Map = ({ position, setPosition, center }: MapProps) => { + const t = useTranslations("Map"); + + const latValue = position.lat.toFixed(4); + const lngValue = position.lng.toFixed(4); + + const setMapBounds = useSetAtom(mapBoundsAtom); + const markerRef = useRef(null); + + const handleLatChange = ({ target }: ChangeEvent) => { + const newLat = Number(target.value); + setPosition((prevPosition) => ({ ...prevPosition, lat: newLat })); + }; + + const handleLngChange = ({ target }: ChangeEvent) => { + const newLng = Number(target.value); + setPosition((prevPosition) => ({ ...prevPosition, lng: newLng })); + }; + + const eventHandlers = useMemo( + () => ({ + dragend() { + const marker = markerRef.current; + if (marker != null) { + setPosition(marker.getLatLng()); + } + }, + }), + [setPosition], + ); + + return ( + <> +
+ + +
+
+ + +
+ +
+ { + if (map) { + setMapBounds(map.getBounds()); + } + }} + > + + + + +
+ + ); +}; + +export default Map; diff --git a/app/[lang]/ajouter-un-tournoi/TournamentForm.tsx b/app/[lang]/ajouter-un-tournoi/TournamentForm.tsx new file mode 100644 index 0000000..85164d5 --- /dev/null +++ b/app/[lang]/ajouter-un-tournoi/TournamentForm.tsx @@ -0,0 +1,268 @@ +"use client"; + +import dynamic from "next/dynamic"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useAtomValue } from "jotai"; +import { franceCenterAtom } from "@/app/atoms"; +import useTournamentForm from "@/hooks/useTournamentForm"; +import InfoMessage from "@/app/[lang]/components/InfoMessage"; +import { + handleTournamentSubmit, + handleClearTournamentForm, +} from "@/handlers/formHandlers"; +import LoadingMap from "@/app/[lang]/components/LoadingMap"; + +const Map = dynamic(() => import("./Map"), { + ssr: false, + loading: LoadingMap, +}); + +const TournamentForm = () => { + const t = useTranslations("AddTournament"); + const center = useAtomValue(franceCenterAtom); + const formRefs = useTournamentForm(); + + const [isSending, setIsSending] = useState(false); + const [responseMessage, setResponseMessage] = useState({ + isSuccessful: false, + message: "", + }); + + return ( + <> +
+ handleTournamentSubmit( + e, + t, + setIsSending, + setResponseMessage, + formRefs, + ) + } + className="space-y-8" + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + ; + + + ); +}; + +export default TournamentForm; diff --git a/app/[lang]/ajouter-un-tournoi/page.tsx b/app/[lang]/ajouter-un-tournoi/page.tsx new file mode 100644 index 0000000..9983cdd --- /dev/null +++ b/app/[lang]/ajouter-un-tournoi/page.tsx @@ -0,0 +1,24 @@ +import { useTranslations } from "next-intl"; + +import TournamentForm from "./TournamentForm"; + +export default function Contact() { + const t = useTranslations("AddTournament"); + + return ( +
+
+

+ {t("title")} +

+

+ {t("info")} +

+ +
+
+ ); +} diff --git a/app/[lang]/components/InfoMessage.tsx b/app/[lang]/components/InfoMessage.tsx new file mode 100644 index 0000000..e8af19e --- /dev/null +++ b/app/[lang]/components/InfoMessage.tsx @@ -0,0 +1,30 @@ +import { ResponseMessage } from "@/types"; +import { Dispatch, SetStateAction } from "react"; + +const InfoMessage = ({ + responseMessage, +}: { + responseMessage: ResponseMessage; +}) => ( +

+ {responseMessage.message} +

+); + +export const clearMessage = ( + setResponseMessage: Dispatch>, +) => { + setTimeout(() => { + setResponseMessage({ + isSuccessful: false, + message: "", + }); + }, 10000); +}; + +export default InfoMessage; diff --git a/app/[lang]/components/LoadingMap.tsx b/app/[lang]/components/LoadingMap.tsx new file mode 100644 index 0000000..5c49ce9 --- /dev/null +++ b/app/[lang]/components/LoadingMap.tsx @@ -0,0 +1,12 @@ +import { useTranslations } from "next-intl"; + +const LoadingMap = () => { + const t = useTranslations("Tournaments"); + return ( +
+

{t("loading")}

+
+ ); +}; + +export default LoadingMap; diff --git a/app/[lang]/components/MapEvents.tsx b/app/[lang]/components/MapEvents.tsx new file mode 100644 index 0000000..7fa37fc --- /dev/null +++ b/app/[lang]/components/MapEvents.tsx @@ -0,0 +1,31 @@ +import { useEffect } from "react"; +import { useSetAtom } from "jotai"; +import { mapBoundsAtom } from "@/app/atoms"; +import L from "leaflet"; +import { useMapEvent } from "react-leaflet"; + +const MapEvents = () => { + const setMapBounds = useSetAtom(mapBoundsAtom); + + const worldBounds = L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180)); + const franceBounds = L.latLngBounds( + L.latLng(42.08, -5.12), + L.latLng(51.17, 9.53), + ); + + const map = useMapEvent("moveend", () => { + // Set the map bounds atoms when the user pans/zooms + setMapBounds(map.getBounds()); + }); + + // viewport agnostic centering of France & max world bounds + useEffect(() => { + map.setView(franceBounds.getCenter(), map.getBoundsZoom(franceBounds)); + map.setMaxBounds(worldBounds); + map.options.maxBoundsViscosity = 1.0; // Prevents going past bounds while dragging + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return null; +}; + +export default MapEvents; diff --git a/app/[lang]/contactez-nous/ContactForm.tsx b/app/[lang]/contactez-nous/ContactForm.tsx index f4c5007..8505744 100644 --- a/app/[lang]/contactez-nous/ContactForm.tsx +++ b/app/[lang]/contactez-nous/ContactForm.tsx @@ -1,70 +1,37 @@ "use client"; -import { useState, FormEvent } from "react"; +import { useState } from "react"; import { useTranslations } from "next-intl"; -import sendMail from "@/lib/sendMail"; -import { errorLog } from "@/utils/logger"; import useContactForm from "@/hooks/useContactForm"; +import { handleEmailSubmit } from "@/handlers/formHandlers"; + +import InfoMessage from "@/app/[lang]/components/InfoMessage"; const ContactForm = () => { const t = useTranslations("Contact"); const { values, handleChange, resetForm } = useContactForm(); + const [isSending, setIsSending] = useState(false); const [responseMessage, setResponseMessage] = useState({ isSuccessful: false, message: "", }); - const [isSending, setIsSending] = useState(false); - - const clearMessage = () => { - setTimeout(() => { - setResponseMessage({ - isSuccessful: false, - message: "", - }); - }, 10000); - }; - - const handleEmailSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsSending(true); - try { - const response = await sendMail(values); - if (response.status === 250) { - setResponseMessage({ - isSuccessful: true, - message: t("success"), - }); - - resetForm(); - clearMessage(); - setIsSending(false); - } - } catch (error) { - errorLog(error); - setResponseMessage({ - isSuccessful: false, - message: t("failure"), - }); - clearMessage(); - setIsSending(false); - } - }; - - const infoMessage = ( -

- {responseMessage.message} -

- ); return ( <> -
+ + handleEmailSubmit( + e, + t, + setIsSending, + setResponseMessage, + values, + resetForm, + ) + } + className="space-y-8" + >