mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-22 20:16:57 +00:00
Avatar with drop down menu for accessing zones and signing out
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/mongodb-adapter": "^2.5.0",
|
||||
"@headlessui-float/react": "^0.13.3",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@hookform/resolvers": "^3.3.3",
|
||||
|
||||
@@ -3,11 +3,13 @@ import { useTranslations } from "next-intl";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { burgerMenuIsOpenAtom } from "@/atoms";
|
||||
import { useAuthMenuOptions } from "@/hooks/useAuthMenuOptions";
|
||||
import { Link } from "@/utils/navigation";
|
||||
|
||||
const HamburgerMenu = () => {
|
||||
const t = useTranslations("Nav");
|
||||
const [burgerMenuIsOpen, setBurgerMenuIsOpen] = useAtom(burgerMenuIsOpenAtom);
|
||||
const menuItems = useAuthMenuOptions();
|
||||
|
||||
const closeMenu = () => setBurgerMenuIsOpen(false);
|
||||
|
||||
@@ -20,35 +22,41 @@ const HamburgerMenu = () => {
|
||||
)}
|
||||
>
|
||||
<ul className="list-reset text-white">
|
||||
<li className="py-5 text-center text-xl">
|
||||
<Link
|
||||
onClick={closeMenu}
|
||||
href="/tournaments"
|
||||
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
|
||||
>
|
||||
<li className="py-3 text-center text-xl">
|
||||
<Link onClick={closeMenu} href="/tournaments">
|
||||
{t("tournaments")}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li className="py-5 text-center text-xl">
|
||||
<Link
|
||||
onClick={closeMenu}
|
||||
href="/clubs"
|
||||
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
|
||||
>
|
||||
<li className="py-3 text-center text-xl">
|
||||
<Link onClick={closeMenu} href="/clubs">
|
||||
{t("clubs")}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li className="py-5 text-center text-xl">
|
||||
<Link
|
||||
onClick={closeMenu}
|
||||
href="/elo"
|
||||
className="border-b-2 border-transparent transition-all duration-300 ease-in-out hover:border-white"
|
||||
>
|
||||
<li className="py-3 text-center text-xl">
|
||||
<Link onClick={closeMenu} href="/elo">
|
||||
{t("elo")}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<hr className="my-5" />
|
||||
|
||||
{menuItems.map(({ title, onClick, className, disabled }, i) => (
|
||||
<li key={i} className="py-3 text-center text-xl">
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
onClick={() => {
|
||||
onClick();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,10 +15,7 @@ export default function Navbar() {
|
||||
] as const;
|
||||
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<div className="container mx-auto flex h-full items-center justify-between">
|
||||
<Link
|
||||
className="font-extrabold text-gray-900 no-underline hover:no-underline dark:text-white"
|
||||
@@ -33,10 +30,7 @@ export default function Navbar() {
|
||||
<Hamburger />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="hidden h-full justify-center md:flex md:w-1/2 md:justify-end"
|
||||
data-test="desktop-menu"
|
||||
>
|
||||
<div className="hidden h-full justify-center md:flex md:w-1/2 md:justify-end">
|
||||
<ul className="list-reset flex h-full flex-1 items-center gap-14 text-gray-900 no-underline dark:text-white md:flex-none">
|
||||
{links.map(({ title, route }) => (
|
||||
<li key={route} className="h-full">
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function Contact() {
|
||||
const t = useTranslations("Contact");
|
||||
|
||||
return (
|
||||
<section className="grid place-items-center bg-white pb-20 dark:bg-gray-800">
|
||||
<section className="bg-white pb-20 dark:bg-gray-800">
|
||||
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
|
||||
<h2
|
||||
className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
|
||||
|
||||
@@ -5,6 +5,7 @@ import "leaflet-defaulticon-compatibility";
|
||||
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { FeatureGroup, GeoJSON, MapContainer, TileLayer } from "react-leaflet";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { MapEvents } from "@/components/MapEvents";
|
||||
import type { ZoneModel } from "@/server/models/zoneModel";
|
||||
@@ -13,10 +14,10 @@ const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
||||
|
||||
export type ZoneThumbnailProps = {
|
||||
features: ZoneModel["features"];
|
||||
size: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ZoneThumbnail = ({ features, size }: ZoneThumbnailProps) => {
|
||||
export const ZoneThumbnail = ({ features, className }: ZoneThumbnailProps) => {
|
||||
return (
|
||||
<MapContainer
|
||||
center={center}
|
||||
@@ -24,7 +25,7 @@ export const ZoneThumbnail = ({ features, size }: ZoneThumbnailProps) => {
|
||||
zoomControl={false}
|
||||
scrollWheelZoom={false}
|
||||
dragging={false}
|
||||
style={{ height: size, width: size }}
|
||||
className={twMerge("h-[200px] w-auto md:w-[200px]", className)}
|
||||
attributionControl={false}
|
||||
>
|
||||
<MapEvents bounds={L.geoJson(features).getBounds()} />
|
||||
|
||||
@@ -20,25 +20,27 @@ export default function ZonesLayout({ children }: { children: ReactNode }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
|
||||
{!sessionData ? (
|
||||
<div>
|
||||
<h2
|
||||
className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
|
||||
data-test="header2"
|
||||
>
|
||||
{t("title")}
|
||||
</h2>
|
||||
<section className="bg-white pb-20 dark:bg-gray-800">
|
||||
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
|
||||
{!sessionData ? (
|
||||
<div>
|
||||
<h2
|
||||
className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
|
||||
data-test="header2"
|
||||
>
|
||||
{t("title")}
|
||||
</h2>
|
||||
|
||||
<p className="mb-8 text-center font-light text-gray-500 dark:text-gray-400 sm:text-xl lg:mb-16">
|
||||
{t("logonRequired")}
|
||||
</p>
|
||||
<p className="mb-8 text-center font-light text-gray-500 dark:text-gray-400 sm:text-xl lg:mb-16">
|
||||
{t("logonRequired")}
|
||||
</p>
|
||||
|
||||
<SignInForm />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
<SignInForm />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,12 +86,17 @@ const Zones = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-4 text-gray-900 dark:text-white"
|
||||
className="flex flex-col gap-4 text-gray-900 dark:text-white md:flex-row"
|
||||
key={zone.id}
|
||||
>
|
||||
<ZoneThumbnail features={zone.features} size={200} />
|
||||
<div className="flex flex-1 flex-col justify-between">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white md:hidden">
|
||||
{zone.name}
|
||||
</h3>
|
||||
|
||||
<ZoneThumbnail features={zone.features} />
|
||||
|
||||
<div className="flex flex-1 flex-col justify-between gap-4">
|
||||
<h3 className="hidden text-xl font-semibold text-gray-900 dark:text-white md:block">
|
||||
{zone.name}
|
||||
</h3>
|
||||
|
||||
|
||||
@@ -2,31 +2,36 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Menu } from "@headlessui/react";
|
||||
import { signIn, signOut, useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { RxAvatar } from "react-icons/rx";
|
||||
|
||||
import { useAuthMenuOptions } from "@/hooks/useAuthMenuOptions";
|
||||
|
||||
import { DropdownMenu } from "./DropdownMenu";
|
||||
|
||||
const AuthButton = () => {
|
||||
const t = useTranslations("Nav");
|
||||
const router = useRouter();
|
||||
const [signingOut, setSigningOut] = useState(false);
|
||||
const { data: sessionData } = useSession();
|
||||
const { data: sessionData, status } = useSession();
|
||||
const menuItems = useAuthMenuOptions();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
setSigningOut(true);
|
||||
try {
|
||||
await signOut({ redirect: false });
|
||||
router.push("/");
|
||||
} finally {
|
||||
setSigningOut(false);
|
||||
}
|
||||
};
|
||||
if (status === "loading") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sessionData) {
|
||||
return (
|
||||
<button disabled={signingOut} onClick={handleSignOut}>
|
||||
{t("signOut")}
|
||||
</button>
|
||||
<DropdownMenu
|
||||
items={menuItems}
|
||||
containerClassName="flex items-center justify-center"
|
||||
buttonComponent={
|
||||
<Menu.Button>
|
||||
<RxAvatar className="h-6 w-6" />
|
||||
</Menu.Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
|
||||
import { Float } from "@headlessui-float/react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
import { IoMdMore } from "react-icons/io";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type DropdownMenuItem = {
|
||||
title: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type DropdownMenuProps = {
|
||||
items: DropdownMenuItem[];
|
||||
buttonComponent?: React.ReactElement;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenu = ({
|
||||
items,
|
||||
buttonComponent,
|
||||
containerClassName,
|
||||
}: DropdownMenuProps) => {
|
||||
return (
|
||||
<Menu as="div" className={twMerge("relative", containerClassName)}>
|
||||
<Float
|
||||
portal
|
||||
placement="bottom-end"
|
||||
offset={15}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{buttonComponent ?? (
|
||||
<Menu.Button>
|
||||
<IoMdMore />
|
||||
</Menu.Button>
|
||||
)}
|
||||
|
||||
<Menu.Items className="z-20 flex w-auto items-start overflow-hidden rounded-md bg-neutral-200 focus:outline-none dark:bg-neutral-600">
|
||||
<div className="flex flex-col px-1 py-1">
|
||||
{items.map(({ title, onClick, className, disabled }, i) => (
|
||||
<Menu.Item key={i}>
|
||||
<button
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"w-full whitespace-nowrap rounded-md px-2.5 py-2 text-left text-sm text-black dark:text-white",
|
||||
!disabled && "hover:bg-white dark:hover:bg-neutral-700",
|
||||
disabled && "text-neutral-500 dark:text-neutral-500",
|
||||
className,
|
||||
)}
|
||||
disabled={disabled ?? false}
|
||||
onClick={() => {
|
||||
if (!disabled) onClick();
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Float>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
const LoadingMap = () => {
|
||||
const t = useTranslations("Tournaments");
|
||||
return (
|
||||
<div className="grid h-content place-self-center bg-white text-center text-gray-900 dark:bg-gray-800 dark:text-white">
|
||||
<p>{t("loading")}</p>
|
||||
<div className="mt-20 flex h-content justify-center bg-white pt-20 text-center text-gray-900 dark:bg-gray-800 dark:text-white">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { signIn, signOut, useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { DropdownMenuItem } from "@/components/DropdownMenu";
|
||||
|
||||
export const useAuthMenuOptions = () => {
|
||||
const t = useTranslations("Nav");
|
||||
const router = useRouter();
|
||||
const [signingOut, setSigningOut] = useState(false);
|
||||
const { data: sessionData, status } = useSession();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
setSigningOut(true);
|
||||
try {
|
||||
await signOut({ redirect: false });
|
||||
router.push("/");
|
||||
} finally {
|
||||
setSigningOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "loading") {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!sessionData) {
|
||||
return [
|
||||
{
|
||||
title: t("signIn"),
|
||||
onClick: () => signIn(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const items: DropdownMenuItem[] = [
|
||||
{
|
||||
title: t("myZones"),
|
||||
onClick: () => router.push("/zones"),
|
||||
disabled: signingOut,
|
||||
},
|
||||
// {
|
||||
// title: t("deleteAccount"),
|
||||
// onClick: handleSignOut,
|
||||
// disabled: signingOut,
|
||||
// },
|
||||
{
|
||||
title: t("signOut"),
|
||||
onClick: handleSignOut,
|
||||
disabled: signingOut,
|
||||
},
|
||||
];
|
||||
|
||||
return items;
|
||||
};
|
||||
@@ -35,7 +35,9 @@
|
||||
"clubs": "Clubs",
|
||||
"elo": "Elo Calculator",
|
||||
"signIn": "Sign In",
|
||||
"signOut": "Sign Out"
|
||||
"signOut": "Sign Out",
|
||||
"myZones": "My Zones",
|
||||
"deleteAccount": "Delete my Account"
|
||||
},
|
||||
|
||||
"Footer": {
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
"clubs": "Clubs",
|
||||
"elo": "Calculette Elo",
|
||||
"signIn": "Se connecter",
|
||||
"signOut": "Se déconnecter"
|
||||
"signOut": "Se déconnecter",
|
||||
"myZones": "Mes Régions",
|
||||
"deleteAccount": "Supprimer le compte"
|
||||
},
|
||||
|
||||
"Footer": {
|
||||
|
||||
@@ -1178,14 +1178,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
|
||||
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
|
||||
|
||||
"@floating-ui/core@^1.0.0":
|
||||
"@floating-ui/core@^1.0.0", "@floating-ui/core@^1.5.3":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
|
||||
integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.1"
|
||||
|
||||
"@floating-ui/dom@^1.0.1", "@floating-ui/dom@^1.6.1":
|
||||
"@floating-ui/dom@^1.0.1", "@floating-ui/dom@^1.5.4", "@floating-ui/dom@^1.6.1":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
|
||||
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
|
||||
@@ -1193,6 +1193,22 @@
|
||||
"@floating-ui/core" "^1.0.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.0":
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d"
|
||||
integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.6.1"
|
||||
|
||||
"@floating-ui/react@^0.26.5":
|
||||
version "0.26.12"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.12.tgz#6908f774d8e3167d89b37fd83be975c7e5d8be99"
|
||||
integrity sha512-D09o62HrWdIkstF2kGekIKAC0/N/Dl6wo3CQsnLcOmO3LkW6Ik8uIb3kw8JYkwxNCcg+uJ2bpWUiIijTBep05w==
|
||||
dependencies:
|
||||
"@floating-ui/react-dom" "^2.0.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
tabbable "^6.0.0"
|
||||
|
||||
"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||
@@ -1259,6 +1275,15 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@headlessui-float/react@^0.13.3":
|
||||
version "0.13.3"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui-float/react/-/react-0.13.3.tgz#700ac6cfe56a264dcdc4c6486253dd7573b1b970"
|
||||
integrity sha512-wApuiUu2zcuETftdEJupEUG3XP6AUpwNN+kb6rrBadMf/jpOt1rVBjurgL+peKSW9AfxpNT7uqlL4se6u/epqw==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.5.3"
|
||||
"@floating-ui/dom" "^1.5.4"
|
||||
"@floating-ui/react" "^0.26.5"
|
||||
|
||||
"@headlessui/react@^1.7.18":
|
||||
version "1.7.18"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.18.tgz#30af4634d2215b2ca1aa29d07f33d02bea82d9d7"
|
||||
@@ -5516,6 +5541,11 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
tabbable@^6.0.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
|
||||
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||
|
||||
tailwind-merge@^2.0.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.2.tgz#87341e7604f0e20499939e152cd2841f41f7a3df"
|
||||
|
||||
Reference in New Issue
Block a user