Avatar with drop down menu for accessing zones and signing out

This commit is contained in:
Timothy Armes
2024-04-12 14:54:44 +02:00
parent 860644bd9f
commit cfbba733c2
14 changed files with 255 additions and 73 deletions
+1
View File
@@ -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">
+1 -1
View File
@@ -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 -18
View File
@@ -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>
);
}
+9 -4
View File
@@ -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>
+20 -15
View File
@@ -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>
}
/>
);
}
+71
View File
@@ -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>
);
};
+4 -2
View File
@@ -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>
);
};
+59
View File
@@ -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;
};
+3 -1
View File
@@ -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": {
+3 -1
View File
@@ -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": {
+32 -2
View File
@@ -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"