From c5a1503684cc3e32e6de734e95d795f86b734f44 Mon Sep 17 00:00:00 2001 From: Timothy Armes Date: Mon, 3 Jul 2023 20:14:19 +0200 Subject: [PATCH] Localisation (#39) * Localisation * Update tests * Fix map markers in English view * Fix typo --- .gitignore | 2 + .vscode/settings.json | 17 ++ app/[lang]/contactez-nous/page.tsx | 28 +++ app/[lang]/layout.tsx | 59 +++++++ app/[lang]/page.tsx | 46 +++++ app/[lang]/qui-sommes-nous/page.tsx | 124 ++++++++++++++ app/{ => [lang]}/tournois/page.tsx | 19 ++- app/contactez-nous/page.tsx | 26 --- app/layout.tsx | 24 --- app/page.tsx | 42 ----- app/qui-sommes-nous/page.tsx | 124 -------------- app/sitemap.ts | 14 +- components/ContactForm.tsx | 82 ++++++--- components/Footer.tsx | 23 ++- components/Hamburger.tsx | 14 +- components/HamburgerMenu.tsx | 25 +-- components/Legend.tsx | 20 ++- components/Navbar.tsx | 30 ++-- components/SearchBar.tsx | 15 +- components/TournamentMap.tsx | 10 +- components/TournamentTable.tsx | 51 +++--- cypress/component/mounting.cy.tsx | 8 +- .../support/{component.ts => component.tsx} | 17 +- cypress/videos/contactForm.cy.ts.mp4 | Bin 285238 -> 0 bytes cypress/videos/data.cy.ts-compressed.mp4 | Bin 21262 -> 0 bytes cypress/videos/data.cy.ts.mp4 | Bin 76357 -> 0 bytes cypress/videos/data.cy.ts.mp4.meta | 11 -- cypress/videos/links.cy.ts.mp4 | Bin 347419 -> 0 bytes cypress/videos/navbar.cy.ts.mp4 | Bin 152571 -> 0 bytes cypress/videos/scroll.cy.ts.mp4 | Bin 216242 -> 0 bytes global.d.ts | 4 + handlers/formSubmitHandlers.ts | 46 ----- i18n.ts | 5 + messages/en.json | 72 ++++++++ messages/fr.json | 72 ++++++++ middleware.ts | 15 ++ next.config.js | 6 +- package-lock.json | 161 ++++++++++++++++++ package.json | 1 + tsconfig.json | 2 +- utils/layerGroups.tsx | 92 ++++++---- 41 files changed, 881 insertions(+), 426 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 app/[lang]/contactez-nous/page.tsx create mode 100644 app/[lang]/layout.tsx create mode 100644 app/[lang]/page.tsx create mode 100644 app/[lang]/qui-sommes-nous/page.tsx rename app/{ => [lang]}/tournois/page.tsx (82%) delete mode 100644 app/contactez-nous/page.tsx delete mode 100644 app/layout.tsx delete mode 100644 app/page.tsx delete mode 100644 app/qui-sommes-nous/page.tsx rename cypress/support/{component.ts => component.tsx} (77%) delete mode 100644 cypress/videos/contactForm.cy.ts.mp4 delete mode 100644 cypress/videos/data.cy.ts-compressed.mp4 delete mode 100644 cypress/videos/data.cy.ts.mp4 delete mode 100644 cypress/videos/data.cy.ts.mp4.meta delete mode 100644 cypress/videos/links.cy.ts.mp4 delete mode 100644 cypress/videos/navbar.cy.ts.mp4 delete mode 100644 cypress/videos/scroll.cy.ts.mp4 create mode 100644 global.d.ts delete mode 100644 handlers/formSubmitHandlers.ts create mode 100644 i18n.ts create mode 100644 messages/en.json create mode 100644 messages/fr.json create mode 100644 middleware.ts diff --git a/.gitignore b/.gitignore index 5f82fe3..74206d9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ TODO # testing /coverage +/cypress/videos +/cypress/screenshots # next.js /.next/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..90c9279 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "cSpell.words": [ + "approximative", + "colour", + "contactez", + "defaulticon", + "Échecs", + "Fédération", + "Française", + "Lente", + "localisation", + "randomisation", + "Rapide", + "sommes", + "tournois" + ] +} diff --git a/app/[lang]/contactez-nous/page.tsx b/app/[lang]/contactez-nous/page.tsx new file mode 100644 index 0000000..490a13d --- /dev/null +++ b/app/[lang]/contactez-nous/page.tsx @@ -0,0 +1,28 @@ +import { useTranslations } from "next-intl"; + +import Layout from "@/components/Layout"; +import ContactForm from "@/components/ContactForm"; + +// TODO fix page sizing +export default function Contact() { + const t = useTranslations("Contact"); + + return ( + +
+
+

+ {t("title")} +

+

+ {t("info")} +

+ +
+
+
+ ); +} diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx new file mode 100644 index 0000000..f37ca00 --- /dev/null +++ b/app/[lang]/layout.tsx @@ -0,0 +1,59 @@ +import { Analytics } from "@vercel/analytics/react"; +import { Inter } from "next/font/google"; +import { useLocale, NextIntlClientProvider } from "next-intl"; +import { getTranslator } from "next-intl/server"; +import { notFound } from "next/navigation"; +import { ReactNode } from "react"; + +import "@/app/globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export async function generateMetadata({ + params: { locale }, +}: { + params: { locale?: string }; +}) { + // While the `locale` is required, the namespace is optional and + // identical to the parameter that `useTranslations` accepts. + const t = await getTranslator(locale ?? "fr", "Metadata"); + + return { + title: t("title"), + description: t("description"), + keywords: t("keywords"), + }; +} + +export default async function RootLayout({ + children, + params, +}: { + children: ReactNode; + params: { locale?: string }; +}) { + const locale = useLocale(); + + // Show a 404 error if the user requests an unknown locale + if (params.locale !== undefined && params.locale !== locale) { + notFound(); + } + + let messages; + try { + messages = (await import(`@/messages/${locale}.json`)).default; + } catch (error) { + notFound(); + } + + return ( + + + + {children} + + + + + ); +} diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx new file mode 100644 index 0000000..633f262 --- /dev/null +++ b/app/[lang]/page.tsx @@ -0,0 +1,46 @@ +import Link from "next/link"; +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import Layout from "@/components/Layout"; +import bgImage from "@/public/images/map-bg.jpg"; + +export default function Home() { + const t = useTranslations("Home"); + + return ( + +
+
+ Background image of France +
+
+

+ {t("title")} +

+

{t("purpose")}

+

+ {t.rich("how", { + link: (chunks) => ( + + {chunks} + + ), + })} +

+ + {t("mapLink")} + +
+
+
+ ); +} diff --git a/app/[lang]/qui-sommes-nous/page.tsx b/app/[lang]/qui-sommes-nous/page.tsx new file mode 100644 index 0000000..7d66e8f --- /dev/null +++ b/app/[lang]/qui-sommes-nous/page.tsx @@ -0,0 +1,124 @@ +import Link from "next-intl/link"; +import { useTranslations } from "next-intl"; + +import Layout from "@/components/Layout"; + +export default function About() { + const t = useTranslations("About"); + + return ( + +
+
+

+ {t("title")} +

+

+ {t("p1")} +

+

+ {t.rich("p2", { + homeLink: (chunks) => ( + + {chunks} + + ), + })} +

+

+ {t("p3")} +

+

+ {t("thanksTitle")} +

+

+ {t("thanksTitle")} +

+
    +
  • + + AlvaroNW + +
  • +
  • + + Florifourchette + +
  • +
+

+ {t("techTitle")} +

+

+ {t.rich("techInfo", { + homeLink: (chunks) => ( + + {chunks} + + ), + })} +

+
    +
  • + + NextJS + +
  • +
  • + + Typescript + +
  • +
  • + + Tailwind + +
  • +
  • + + MongoDB + +
  • +
+

+ {t("techWith")} +

+
    +
  • + + Leaflet + +
  • +
  • + + Nodemailer + +
  • +
+

+ {t.rich("contribInfo", { + link: (chunks) => ( + + {chunks} + + ), + })} +

+
+
+
+ ); +} diff --git a/app/tournois/page.tsx b/app/[lang]/tournois/page.tsx similarity index 82% rename from app/tournois/page.tsx rename to app/[lang]/tournois/page.tsx index ba342e0..010658a 100644 --- a/app/tournois/page.tsx +++ b/app/[lang]/tournois/page.tsx @@ -1,11 +1,22 @@ import clientPromise from "@/lib/mongodb"; import dynamic from "next/dynamic"; +import { useTranslations } from "next-intl"; + import Layout from "@/components/Layout"; import TournamentTable from "@/components/TournamentTable"; import { dateOrderingFrance } from "@/utils/dbDateOrdering"; import { errorLog } from "@/utils/logger"; -export const revalidate = 3600; // revalidate cache every 6 hours +export const revalidate = 3600; // revalidate cache every 6 hours; + +const LoadingMap = () => { + const t = useTranslations("Competitions"); + return ( +
+

{t("loading")}

+
+ ); +}; /** * Imports the tournament map component, ensuring CSR only. @@ -13,11 +24,7 @@ export const revalidate = 3600; // revalidate cache every 6 hours */ const TournamentMap = dynamic(() => import("@/components/TournamentMap"), { ssr: false, - loading: () => ( -
-

Loading map...

-
- ), + loading: LoadingMap, }); const getTournaments = async () => { diff --git a/app/contactez-nous/page.tsx b/app/contactez-nous/page.tsx deleted file mode 100644 index 1ca52c4..0000000 --- a/app/contactez-nous/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import Layout from "@/components/Layout"; -import ContactForm from "@/components/ContactForm"; - -// TODO fix page sizing -export default function Contact() { - return ( - -
-
-

- Contactez-Nous -

-

- Vous souhaitez ajouter les tournois de votre fédération sur ce site? - Vous avez un problème technique? Vous aimeriez participer à ce - projet? Contactez-nous. -

- -
-
-
- ); -} diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index bc99daa..0000000 --- a/app/layout.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Analytics } from "@vercel/analytics/react"; - -import "./globals.css"; -import { Inter } from "next/font/google"; -import { ReactNode } from "react"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata = { - title: "Echecs France- Tournois d'Echecs", - description: "Trouvez Vos Tournois d'Echecs en France Sur Une Carte", - keywords: "echecs, France, tournoi, tournois, FFE, carte", -}; - -export default function RootLayout({ children }: { children: ReactNode }) { - return ( - - - {children} - - - - ); -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 1f60bcd..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Link from "next/link"; -import Image from "next/image"; -import Layout from "@/components/Layout"; -import bgImage from "@/public/images/map-bg.jpg"; - -export default function Home() { - return ( - -
-
- Background image of France -
-
-

- Echecs France -

-

- Trouvez Vos Tournois d'Echecs en France Sur Une Carte -

-

- Une représentation visuelle de tous les tournois d'échecs - publiés par la{" "} - - FFE - -

- - Voir La Carte - -
-
-
- ); -} diff --git a/app/qui-sommes-nous/page.tsx b/app/qui-sommes-nous/page.tsx deleted file mode 100644 index 03ac626..0000000 --- a/app/qui-sommes-nous/page.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import Link from "next/link"; -import Layout from "@/components/Layout"; - -export default function About() { - return ( - -
-
-

- Qui Sommes-Nous? -

-

- Ce projet a vu le jour début 2022 afin de permettre une - visualisation sur une carte des tournois d'échecs en France. - Ayant déménagé en France en 2019, je ne connaissais alors pas la - géographie française et je souhaitais savoir quels tournois avaient - lieu près de chez moi. -

-

- Le projet a été mis de côté jusqu'au printemps 2023; date à - laquelle je lui ai redonné vie. Reconstruit à partir de zéro,{" "} - - Echecs France - {" "} - est désormais open source et ouvert aux contributions. -

-

- Je compte mettre en place un bouton de don en ligne pour ceux qui - souhaitent participer aux frais associés au site. Une fois les coûts - de fonctionnement déduits, tous les fonds restant seront redirigés - vers le monde des échecs soit en sponsorant des événements ou par la - création de dons. -

-

- Remerciements -

-

- Ce projet est ce qu'il est grâce aux contributions de vous - tous. Je souhaite en particulier remercier les personnes suivantes - pour leurs contributions: -

-
    -
  • - - AlvaroNW - -
  • -
  • - - Florifourchette - -
  • -
-

- Tech Blurb -

-

- - Echecs France - {" "} - utilise: -

-
    -
  • - - NextJS - -
  • -
  • - - Typescript - -
  • -
  • - - Tailwind - -
  • -
  • - - MongoDB - -
  • -
-

- avec -

-
    -
  • - - Leaflet - -
  • -
  • - - Nodemailer - -
  • -
-

- Pour plus d'informations sur les moyens de contribution, - veuillez visiter notre{" "} - - GitHub. - -

-
-
-
- ); -} diff --git a/app/sitemap.ts b/app/sitemap.ts index ad72599..924f7c7 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,22 +1,24 @@ import { MetadataRoute } from "next"; export default function sitemap(): MetadataRoute.Sitemap { - return [ + return ['fr', 'en'].flatMap(locale => { + const prefix = `https://echecsfrance.com/${locale === 'fr' ? '' : `${locale}/`}` + return [ { - url: "https://echecsfrance.com", + url: prefix, lastModified: new Date(), }, { - url: "https://echecsfrance.com/tournois", + url: `${prefix}tournois`, lastModified: new Date(), }, { - url: "https://echecsfrance.com/qui-sommes-nous", + url: `${prefix}qui-sommes-nous`, lastModified: new Date(), }, { - url: "https://echecsfrance.com/contactez-nous", + url: `${prefix}contactez-nous`, lastModified: new Date(), }, - ]; + ]}); } diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx index 3202b76..1045880 100644 --- a/components/ContactForm.tsx +++ b/components/ContactForm.tsx @@ -1,10 +1,14 @@ "use client"; -import { useState } from "react"; -import { handleEmailSubmit } from "@/handlers/formSubmitHandlers"; +import { useState, FormEvent } from "react"; +import { useTranslations } from "next-intl"; + +import sendMail from "@/lib/sendMail"; +import { errorLog } from "@/utils/logger"; import useContactForm from "@/hooks/useContactForm"; const ContactForm = () => { + const t = useTranslations("Contact"); const { values, handleChange, resetForm } = useContactForm(); const [responseMessage, setResponseMessage] = useState({ isSuccessful: false, @@ -12,6 +16,41 @@ const ContactForm = () => { }); 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 = (

{ return ( <> -

- handleEmailSubmit( - e, - values, - setResponseMessage, - resetForm, - setIsSending - ) - } - className="space-y-8" - > +
{
@@ -76,17 +104,17 @@ const ContactForm = () => {
@@ -94,10 +122,10 @@ const ContactForm = () => { {infoMessage} diff --git a/components/Footer.tsx b/components/Footer.tsx index 1c250f4..da9ae24 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -1,25 +1,38 @@ -import Link from "next/link"; +import Link from "next-intl/link"; import { FaGithub, FaRegEnvelope } from "react-icons/fa"; +import { useTranslations } from "next-intl"; + export default function Footer() { + const t = useTranslations("Footer"); + return (

© {new Date().getFullYear()} - Owen Rees

-
+
- + +
+ + FR + + | + + EN + +
); diff --git a/components/Hamburger.tsx b/components/Hamburger.tsx index 05941de..9d10743 100644 --- a/components/Hamburger.tsx +++ b/components/Hamburger.tsx @@ -10,23 +10,23 @@ const Hamburger = () => { <>
setMenuVisible(!menuVisible)} >
diff --git a/components/HamburgerMenu.tsx b/components/HamburgerMenu.tsx index a37624c..6a7d7b4 100644 --- a/components/HamburgerMenu.tsx +++ b/components/HamburgerMenu.tsx @@ -4,9 +4,11 @@ interface HamburgerMenuState { hamburgerButtonRef: RefObject; } -import Link from "next/link"; -import ThemeSwitcher from "@/components/ThemeSwitcher"; import { Dispatch, RefObject, SetStateAction, useRef, useState } from "react"; +import Link from "next-intl/link"; +import { useTranslations } from "next-intl"; + +import ThemeSwitcher from "@/components/ThemeSwitcher"; import useHamburgerClose from "@/hooks/useHamburgerClose"; const HamburgerMenu = ({ @@ -14,6 +16,7 @@ const HamburgerMenu = ({ setMenuVisible, hamburgerButtonRef, }: HamburgerMenuState) => { + const t = useTranslations("Nav"); const [mouseOverMenu, setMouseOverMenu] = useState(false); const timeoutRef = useRef(); const menuRef = useRef(null); @@ -50,35 +53,35 @@ const HamburgerMenu = ({ return (
-