mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Fix layout issues & co-locate components (#46)
* Fix layout issues & co-locate components * Fix front page * Fix centering
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
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="fixed bottom-0 z-30 flex h-20 w-full flex-col items-center justify-center justify-items-center bg-teal-600 px-5 py-2 text-white dark:bg-gray-700"
|
||||
data-test="footer"
|
||||
>
|
||||
<div>
|
||||
<p>© {new Date().getFullYear()} - Owen Rees</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 p-2">
|
||||
<Link
|
||||
href="https://github.com/TheRealOwenRees/echecsfrance"
|
||||
target="_blank"
|
||||
aria-label={t("githubAria")}
|
||||
>
|
||||
<FaGithub />
|
||||
</Link>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import HamburgerMenu from "./HamburgerMenu";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
const Hamburger = () => {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const hamburgerButtonRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={hamburgerButtonRef}
|
||||
className="hamburger-button relative z-[99999] space-y-2"
|
||||
data-test="hamburger-button"
|
||||
onClick={() => setMenuVisible(!menuVisible)}
|
||||
>
|
||||
<div
|
||||
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={`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={`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>
|
||||
|
||||
<HamburgerMenu
|
||||
menuVisible={menuVisible}
|
||||
setMenuVisible={setMenuVisible}
|
||||
hamburgerButtonRef={hamburgerButtonRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hamburger;
|
||||
@@ -0,0 +1,95 @@
|
||||
interface HamburgerMenuState {
|
||||
menuVisible: boolean;
|
||||
setMenuVisible: Dispatch<SetStateAction<boolean>>;
|
||||
hamburgerButtonRef: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
import { Dispatch, RefObject, SetStateAction, useRef, useState } from "react";
|
||||
import Link from "next-intl/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import useHamburgerClose from "@/hooks/useHamburgerClose";
|
||||
import ThemeSwitcher from "./ThemeSwitcher";
|
||||
|
||||
const HamburgerMenu = ({
|
||||
menuVisible,
|
||||
setMenuVisible,
|
||||
hamburgerButtonRef,
|
||||
}: HamburgerMenuState) => {
|
||||
const t = useTranslations("Nav");
|
||||
const [mouseOverMenu, setMouseOverMenu] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | undefined>();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const menuTimeout = () => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setMenuVisible(false);
|
||||
setMouseOverMenu(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleMouseEnterMenu = () => {
|
||||
setMouseOverMenu(true);
|
||||
clearTimeout(timeoutRef.current);
|
||||
};
|
||||
|
||||
const handleMouseLeaveMenu = () => {
|
||||
setMouseOverMenu(false);
|
||||
clearTimeout(timeoutRef.current);
|
||||
};
|
||||
|
||||
useHamburgerClose({
|
||||
menuVisible,
|
||||
setMenuVisible,
|
||||
menuRef,
|
||||
hamburgerButtonRef,
|
||||
mouseOverMenu,
|
||||
setMouseOverMenu,
|
||||
timeoutRef,
|
||||
menuTimeout,
|
||||
});
|
||||
|
||||
// noinspection HtmlUnknownTarget
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`fixed right-0 top-0 ${
|
||||
menuVisible ? "" : "translate-x-full"
|
||||
} z-[9999] flex bg-teal-600 transition-transform duration-200 ease-linear dark:bg-gray-600 md:hidden`}
|
||||
onMouseEnter={handleMouseEnterMenu}
|
||||
onMouseLeave={handleMouseLeaveMenu}
|
||||
>
|
||||
<ul className="list-reset mt-16 p-5 text-white">
|
||||
<li className="py-5">
|
||||
<Link
|
||||
href="/tournois"
|
||||
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
|
||||
>
|
||||
{t("tournaments")}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="py-5">
|
||||
<Link
|
||||
href="/qui-sommes-nous"
|
||||
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
|
||||
>
|
||||
{t("about")}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="py-5">
|
||||
<Link
|
||||
href="/contactez-nous"
|
||||
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
|
||||
>
|
||||
{t("contact")}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="py-5">
|
||||
<ThemeSwitcher />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HamburgerMenu;
|
||||
@@ -0,0 +1,65 @@
|
||||
import Link from "next-intl/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import ThemeSwitcher from "./ThemeSwitcher";
|
||||
import Hamburger from "./Hamburger";
|
||||
|
||||
export default function Navbar() {
|
||||
const t = useTranslations("Nav");
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="relative mt-0 h-16 w-full overflow-x-clip border-b-[1px] bg-white px-5 dark:border-gray-700 dark:bg-gray-800"
|
||||
data-test="navbar"
|
||||
>
|
||||
<div className="container mx-auto flex h-full items-center">
|
||||
<div className="flex w-full justify-center font-extrabold md:w-1/2 md:justify-start">
|
||||
<Link
|
||||
className="text-gray-900 no-underline hover:no-underline dark:text-white"
|
||||
href="/"
|
||||
>
|
||||
<span className="text-2xl">{t("title")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="pb-2 md:hidden" data-test="mobile-menu">
|
||||
<Hamburger />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="hidden h-full justify-center md:flex md:w-1/2 md:justify-end"
|
||||
data-test="desktop-menu"
|
||||
>
|
||||
<ul className="list-reset flex h-full flex-1 items-center justify-around text-gray-900 no-underline dark:text-white md:flex-none">
|
||||
<li className="mr-10 h-full">
|
||||
<Link
|
||||
className="inline-flex h-full items-center border-b-4 border-t-4 border-transparent transition-all duration-300 ease-in-out hover:border-b-teal-600"
|
||||
href="/tournois"
|
||||
>
|
||||
{t("tournaments")}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mr-10 h-full">
|
||||
<Link
|
||||
className="inline-flex h-full items-center border-b-4 border-t-4 border-transparent transition-all duration-300 ease-in-out hover:border-b-teal-600"
|
||||
href="/qui-sommes-nous"
|
||||
>
|
||||
{t("about")}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mr-10 h-full">
|
||||
<Link
|
||||
className="inline-flex h-full items-center border-b-4 border-t-4 border-transparent transition-all duration-300 ease-in-out hover:border-b-teal-600"
|
||||
href="/contactez-nous"
|
||||
>
|
||||
{t("contact")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<ThemeSwitcher />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextIntlClientProvider, AbstractIntlMessages } from "next-intl";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import "@/app/globals.css";
|
||||
import Navbar from "./Navbar";
|
||||
import Footer from "./Footer";
|
||||
|
||||
const TestableLayout = ({
|
||||
children,
|
||||
locale,
|
||||
messages,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
locale: string;
|
||||
messages: AbstractIntlMessages;
|
||||
}) => {
|
||||
return (
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<div className="flex min-h-screen flex-col bg-white font-sans leading-normal tracking-normal dark:bg-gray-800">
|
||||
<Navbar />
|
||||
<div className="relative mb-20 flex flex-1 justify-center">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</NextIntlClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestableLayout;
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { MdBrightness2, MdCircle } from "react-icons/md";
|
||||
import useDarkMode from "@/hooks/useDarkMode";
|
||||
import { useEffect, useState } from "react";
|
||||
import "@/css/theme-toggle.css";
|
||||
|
||||
const ThemeSwitcher = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [colorTheme, setTheme] = useDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{colorTheme === "light" ? (
|
||||
<div
|
||||
className="toggle-dark"
|
||||
data-test="toggle-dark"
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
<MdBrightness2 className="theme-icon-dark" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="toggle"
|
||||
data-test="toggle"
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
<MdCircle className="theme-icon" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSwitcher;
|
||||
Reference in New Issue
Block a user