Localisation (#39)

* Localisation

* Update tests

* Fix map markers in English view

* Fix typo
This commit is contained in:
Timothy Armes
2023-07-03 20:14:19 +02:00
committed by GitHub
parent 279439f34d
commit c5a1503684
41 changed files with 881 additions and 426 deletions
+55 -27
View File
@@ -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<HTMLFormElement>) => {
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 = (
<p
className={`${
@@ -25,31 +64,20 @@ const ContactForm = () => {
return (
<>
<form
onSubmit={(e) =>
handleEmailSubmit(
e,
values,
setResponseMessage,
resetForm,
setIsSending
)
}
className="space-y-8"
>
<form onSubmit={handleEmailSubmit} className="space-y-8">
<div>
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300"
>
Adresse e-mail
{t("emailLabel")}
</label>
<input
value={values.email}
onChange={handleChange}
type="email"
id="email"
className="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light"
className="focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
placeholder="nom@exemple.com"
required
data-test="email-input"
@@ -58,17 +86,17 @@ const ContactForm = () => {
<div>
<label
htmlFor="subject"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300"
>
Sujet
{t("subjectLabel")}
</label>
<input
value={values.subject}
onChange={handleChange}
type="text"
id="subject"
className="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light"
placeholder="Le motif de ma demande"
className="focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light block w-full rounded-lg border border-gray-300 bg-gray-50 p-3 text-sm text-gray-900 shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
placeholder={t("subjectPlaceholder")}
required
data-test="subject-input"
/>
@@ -76,17 +104,17 @@ const ContactForm = () => {
<div className="sm:col-span-2">
<label
htmlFor="message"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300"
>
Votre message
{t("messageLabel")}
</label>
<textarea
value={values.message}
onChange={handleChange}
id="message"
rows={6}
className="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg shadow-sm border border-gray-300 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Détaillez ici votre demande..."
className="focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
placeholder={t("messagePlaceholder")}
data-test="message-input"
required
></textarea>
@@ -94,10 +122,10 @@ const ContactForm = () => {
<button
disabled={isSending}
type="submit"
className="py-3 px-5 text-sm font-medium text-center text-white rounded-lg bg-teal-600 sm:w-fit hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 dark:hover:bg-primary-700 dark:focus:ring-primary-800 dark:text-white disabled:opacity-25"
className="hover:bg-primary-800 focus:ring-primary-300 dark:hover:bg-primary-700 dark:focus:ring-primary-800 rounded-lg bg-teal-600 px-5 py-3 text-center text-sm font-medium text-white focus:outline-none focus:ring-4 disabled:opacity-25 dark:text-white sm:w-fit"
data-test="submit-button"
>
{isSending ? "Envoi en cours..." : "Envoi Message"}
{isSending ? t("sending") : t("sendButton")}
</button>
{infoMessage}
</form>
+18 -5
View File
@@ -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 (
<footer
className="grid w-full fixed bottom-0 justify-items-center py-2 px-5 text-white bg-teal-600 dark:bg-gray-700 z-30"
className="fixed bottom-0 z-30 grid w-full justify-items-center bg-teal-600 px-5 py-2 text-white dark:bg-gray-700"
data-test="footer"
>
<div className="p-2">
<p>&copy; {new Date().getFullYear()} - Owen Rees</p>
</div>
<div className="flex p-2 space-x-4">
<div className="flex space-x-4 p-2">
<Link
href="https://github.com/TheRealOwenRees/echecsfrance"
target="_blank"
aria-label="Lien vers le dépôt GitHub"
aria-label={t("githubAria")}
>
<FaGithub />
</Link>
<Link href="/contactez-nous" aria-label="Contactez-nous">
<Link href="/contactez-nous" aria-label={t("contactAria")}>
<FaRegEnvelope />
</Link>
<div className="space-x-2 text-xs">
<Link href="/" locale="fr">
FR
</Link>
<span>|</span>
<Link href="/" locale="en">
EN
</Link>
</div>
</div>
</footer>
);
+7 -7
View File
@@ -10,23 +10,23 @@ const Hamburger = () => {
<>
<div
ref={hamburgerButtonRef}
className="hamburger-button space-y-2 relative z-[99999]"
className="hamburger-button relative z-[99999] space-y-2"
data-test="hamburger-button"
onClick={() => setMenuVisible(!menuVisible)}
>
<div
className={`w-8 h-0.5 bg-gray-600 dark:bg-white transition-transform duration-300 ease-in-out ${
menuVisible ? "rotate-45 translate-y-2.5 translate-x-[1px]" : ""
className={`h-0.5 w-8 bg-gray-600 transition-transform duration-300 ease-in-out dark:bg-white ${
menuVisible ? "translate-x-[1px] translate-y-2.5 rotate-45" : ""
}`}
></div>
<div
className={`w-8 h-0.5 bg-gray-600 dark:bg-white transition-transform duration-300 ease-linear ${
menuVisible ? "opacity-0 rotate-0 scale-0" : ""
className={`h-0.5 w-8 bg-gray-600 transition-transform duration-300 ease-linear dark:bg-white ${
menuVisible ? "rotate-0 scale-0 opacity-0" : ""
}`}
></div>
<div
className={`w-8 h-0.5 bg-gray-600 dark:bg-white transition-transform duration-300 ease-in-out ${
menuVisible ? "-rotate-45 -translate-y-2.5" : ""
className={`h-0.5 w-8 bg-gray-600 transition-transform duration-300 ease-in-out dark:bg-white ${
menuVisible ? "-translate-y-2.5 -rotate-45" : ""
}`}
></div>
</div>
+14 -11
View File
@@ -4,9 +4,11 @@ interface HamburgerMenuState {
hamburgerButtonRef: RefObject<HTMLDivElement>;
}
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<NodeJS.Timeout | undefined>();
const menuRef = useRef<HTMLDivElement>(null);
@@ -50,35 +53,35 @@ const HamburgerMenu = ({
return (
<div
ref={menuRef}
className={`absolute top-0 -right-[173px] ${
className={`absolute -right-[173px] top-0 ${
menuVisible ? "-translate-x-full" : ""
} z-[9999] bg-teal-600 flex md:hidden dark:bg-gray-600 transition-transform duration-500 ease-linear`}
} z-[9999] flex bg-teal-600 transition-transform duration-500 ease-linear dark:bg-gray-600 md:hidden`}
onMouseEnter={handleMouseEnterMenu}
onMouseLeave={handleMouseLeaveMenu}
>
<ul className="list-reset text-white mt-16 p-5">
<ul className="list-reset mt-16 p-5 text-white">
<li className="py-5">
<Link
href="/tournois"
className="border-b-2 border-transparent hover:border-white transition-all ease-in-out duration-300"
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
>
Tournois
{t("competitions")}
</Link>
</li>
<li className="py-5">
<Link
href="/qui-sommes-nous"
className="border-b-2 border-transparent hover:border-white transition-all ease-in-out duration-300"
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
>
Qui Sommes-Nous
{t("about")}
</Link>
</li>
<li className="py-5">
<Link
href="/contactez-nous"
className="border-b-2 border-transparent hover:border-white transition-all ease-in-out duration-300"
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
>
Contactez-Nous
{t("contact")}
</Link>
</li>
<li className="py-5">
+15 -5
View File
@@ -1,8 +1,10 @@
import { useMap } from "react-leaflet";
import { useEffect } from "react";
import { useTranslations } from "next-intl";
import L from "leaflet";
const Legend = () => {
const t = useTranslations("App");
const map = useMap();
useEffect(() => {
@@ -17,17 +19,25 @@ const Legend = () => {
"background: white; color: black; border: 2px solid grey; border-radius: 6px; padding: 10px;"
);
div.innerHTML = `<ul>
<li><span style='background:#00ac39; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>Cadence Lente</li>
<li><span style='background:#0086c7; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>Rapide</li>
<li><span style='background:#cec348; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>Blitz</li>
<li><span style='background:#d10c3e; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>1h KO</li>
<li><span style='background:#00ac39; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
"tcClassic"
)}</li>
<li><span style='background:#0086c7; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
"tcRapid"
)}</li>
<li><span style='background:#cec348; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
"tcBlitz"
)}</li>
<li><span style='background:#d10c3e; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
"tcKO"
)}</li>
</ul>`;
return div;
};
legend.addTo(map);
}
}, [map]);
}, [map, t]);
return null;
};
+17 -13
View File
@@ -1,20 +1,24 @@
import Link from "next/link";
import Link from "next-intl/link";
import { useTranslations } from "next-intl";
import ThemeSwitcher from "@/components/ThemeSwitcher";
import Hamburger from "@/components/Hamburger";
export default function Navbar() {
const t = useTranslations("Nav");
return (
<nav
className="w-full relative border-b-[1px] pt-5 mt-0 px-5 md:pt-2 bg-white dark:bg-gray-800 dark:border-gray-700 overflow-x-clip"
className="relative mt-0 w-full overflow-x-clip border-b-[1px] bg-white px-5 pt-5 dark:border-gray-700 dark:bg-gray-800 md:pt-2"
data-test="navbar"
>
<div className="container mx-auto flex items-center">
<div className="pb-3 justify-center flex w-full md:w-1/2 md:pb-0 md:justify-start font-extrabold">
<div className="flex w-full justify-center pb-3 font-extrabold md:w-1/2 md:justify-start md:pb-0">
<Link
className="text-gray-900 dark:text-white no-underline hover:no-underline"
className="text-gray-900 no-underline hover:no-underline dark:text-white"
href="/"
>
<span className="text-2xl">Echecs France</span>
<span className="text-2xl">{t("title")}</span>
</Link>
</div>
<div className="pb-2 md:hidden" data-test="mobile-menu">
@@ -22,32 +26,32 @@ export default function Navbar() {
</div>
<div
className="hidden pt-2 justify-center md:flex md:w-1/2 md:justify-end"
className="hidden justify-center pt-2 md:flex md:w-1/2 md:justify-end"
data-test="desktop-menu"
>
<ul className="list-reset text-gray-900 dark:text-white no-underline flex flex-1 justify-around md:flex-none items-center">
<ul className="list-reset flex flex-1 items-center justify-around text-gray-900 no-underline dark:text-white md:flex-none">
<li className="mr-10">
<Link
className="inline-block border-b-4 py-5 border-transparent hover:border-teal-600 transition-all ease-in-out duration-300"
className="inline-block border-b-4 border-transparent py-5 transition-all duration-300 ease-in-out hover:border-teal-600"
href="/tournois"
>
Tournois
{t("competitions")}
</Link>
</li>
<li className="mr-10">
<Link
className="inline-block border-b-4 py-5 border-transparent hover:border-teal-600 transition-all ease-in-out duration-300"
className="inline-block border-b-4 border-transparent py-5 transition-all duration-300 ease-in-out hover:border-teal-600"
href="/qui-sommes-nous"
>
Qui Sommes-Nous
{t("about")}
</Link>
</li>
<li className="mr-10">
<Link
className="inline-block border-b-4 py-5 border-transparent hover:border-teal-600 transition-all ease-in-out duration-300"
className="inline-block border-b-4 border-transparent py-5 transition-all duration-300 ease-in-out hover:border-teal-600"
href="/contactez-nous"
>
Contactez-Nous
{t("contact")}
</Link>
</li>
<li>
+9 -6
View File
@@ -1,4 +1,5 @@
import { Dispatch, SetStateAction } from "react";
import { useTranslations } from "next-intl";
type SearchBarProps = {
tournamentFilter: string;
@@ -9,15 +10,17 @@ const SearchBar = ({
tournamentFilter,
setTournamentFilter,
}: SearchBarProps) => {
const t = useTranslations("Competitions");
return (
<div className="p-3 bg-white dark:bg-gray-800">
<div className="bg-white p-3 dark:bg-gray-800">
<label htmlFor="table-search" className="sr-only">
Search
{t("searchLabel")}
</label>
<div className="relative mt-1">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg
className="w-5 h-5 text-gray-500 dark:text-gray-400"
className="h-5 w-5 text-gray-500 dark:text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
@@ -32,8 +35,8 @@ const SearchBar = ({
<input
type="text"
id="table-search"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-80 pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Recherche"
className="block w-80 rounded-lg border border-gray-300 bg-gray-50 p-2.5 pl-10 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder={t("searchPlaceholder")}
value={tournamentFilter}
onChange={(e) => setTournamentFilter(e.target.value)}
/>
+8 -2
View File
@@ -6,6 +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 } from "react-leaflet";
import { createLayerGroups } from "@/utils/layerGroups";
@@ -17,8 +18,13 @@ export default function TournamentMap({ tournamentData }: TournamentDataProps) {
const classicalMarkers = createLayerGroups("Cadence Lente", "green", {
tournamentData,
});
const rapidMarkers = createLayerGroups("Rapide", "blue", { tournamentData });
const blitzMarkers = createLayerGroups("Blitz", "yellow", { tournamentData });
const rapidMarkers = createLayerGroups("Rapide", "blue", {
tournamentData,
});
const blitzMarkers = createLayerGroups("Blitz", "yellow", {
tournamentData,
});
const otherMarkers = createLayerGroups("1h KO", "red", { tournamentData });
return (
+27 -24
View File
@@ -2,6 +2,8 @@
import { TournamentDataProps } from "@/types";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import SearchBar from "@/components/SearchBar";
import ScrollToTopButton from "@/components/ScrollToTopButton";
@@ -9,6 +11,7 @@ export default function TournamentTable({
tournamentData,
}: TournamentDataProps) {
let tableData;
const t = useTranslations("Competitions");
const [searchQuery, setSearchQuery] = useState(""); // text from search bar
const [filteredTournamentData, setFilteredTournamentData] =
useState(tournamentData);
@@ -17,41 +20,41 @@ export default function TournamentTable({
setFilteredTournamentData(
tournamentData.filter((t) => t.town.includes(searchQuery.toUpperCase()))
);
}, [searchQuery]);
}, [searchQuery, tournamentData]);
// TODO move this section into its own function
if (filteredTournamentData.length === 0) {
tableData = (
<tr className="bg-white text-gray-900 dark:bg-gray-800 dark:text-white">
<td colSpan={4} className="p-3">
No tournaments found
{t("noneFound")}
</td>
</tr>
);
} else {
tableData = filteredTournamentData.map((t) => (
tableData = filteredTournamentData.map((data) => (
<tr
className="text-gray-900 bg-white dark:bg-gray-800 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
key={t._id}
className="bg-white text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-900"
key={data._id}
>
<td className="p-3">
<a href={t.url} target="_blank">
{t.date}
<a href={data.url} target="_blank">
{data.date}
</a>
</td>
<td className="p-3">
<a href={t.url} target="_blank">
{t.town}
<a href={data.url} target="_blank">
{data.town}
</a>
</td>
<td className="p-3">
<a href={t.url} target="_blank">
{t.tournament}
<a href={data.url} target="_blank">
{data.tournament}
</a>
</td>
<td className="p-3">
<a href={t.url} target="_blank">
{t.time_control}
<a href={data.url} target="_blank">
{data.time_control}
</a>
</td>
</tr>
@@ -60,11 +63,11 @@ export default function TournamentTable({
return (
<section
className="tournament-table w-full grid auto-rows-max pb-20 lg:h-[calc(100vh-173px)] lg:col-start-2 lg:col-end-3 lg:overflow-y-scroll"
className="tournament-table grid w-full auto-rows-max pb-20 lg:col-start-2 lg:col-end-3 lg:h-[calc(100vh-173px)] lg:overflow-y-scroll"
id="tournament-table"
data-test="tournament-table-div"
>
<div className="flex z-10">
<div className="z-10 flex">
<SearchBar
tournamentFilter={searchQuery}
setTournamentFilter={setSearchQuery}
@@ -74,22 +77,22 @@ export default function TournamentTable({
</div>
</div>
<table
className="relative table-fixed w-full text-center text-xs"
className="relative w-full table-fixed text-center text-xs"
data-test="tournament-table"
>
<thead>
<tr>
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
Date
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("date")}
</th>
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
Ville
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("town")}
</th>
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
Tournois
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("competition")}
</th>
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
Cadence
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("timeControl")}
</th>
</tr>
</thead>