Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/components/header/user-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { useClickOutside } from "@hooks";
import clsx from "clsx";
import Image from "next/image";
import Link from "next/link";
import { signOut, useSession } from "next-auth/react";
import { useState } from "react";

type TProps = {
/** Tone of surrounding header — affects placeholder/avatar contrast on dark headers. */
mode?: "light" | "dark";
className?: string;
};

const UserMenu = ({ mode = "dark", className }: TProps) => {
const { data: session, status } = useSession();
const [open, setOpen] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));

const handleLogout = async () => {
setOpen(false);
try {
await signOut({ callbackUrl: "/login" });
} catch (error) {
console.error("[UserMenu] signOut failed:", error);
window.alert(
"Logout failed. Please try again, or clear your browser cookies for this site."
);
}
};

if (status === "loading") {
return (
<div
aria-hidden="true"
className={clsx(
"tw-h-9 tw-w-9 tw-animate-pulse tw-rounded-full tw-border",
mode === "light"
? "tw-border-white/20 tw-bg-white/10"
: "tw-border-navy/10 tw-bg-navy/5",
className
)}
/>
);
}

if (status !== "authenticated" || !session?.user) {
return (
<Link
href="/login"
className={clsx(
"tw-font-mono tw-text-xs tw-font-bold tw-uppercase tw-tracking-widest tw-transition-colors",
mode === "light"
? "tw-text-white hover:tw-text-gold"
: "tw-text-navy hover:tw-text-secondary",
className
)}
>
Sign in
</Link>
);
}

const userId = session.user.id;
const name = session.user.name || "Operator";
const image = session.user.image || null;
const initials = name
.split(" ")
.map((part) => part[0])
.filter(Boolean)
.slice(0, 2)
.join("")
.toUpperCase();

return (
<div ref={ref} className={clsx("tw-relative", className)}>
<button
type="button"
aria-haspopup="menu"
aria-expanded={open}
aria-label={`Account menu for ${name}`}
onClick={() => setOpen((prev) => !prev)}
className="tw-flex tw-h-9 tw-w-9 tw-items-center tw-justify-center tw-overflow-hidden tw-rounded-full tw-border-2 tw-border-gold/40 tw-bg-navy/5 tw-text-xs tw-font-bold tw-text-navy tw-transition-all hover:tw-border-gold focus:tw-outline-none focus-visible:tw-ring-2 focus-visible:tw-ring-gold focus-visible:tw-ring-offset-2"
>
{image ? (
<Image
src={image}
alt=""
width={36}
height={36}
className="tw-h-full tw-w-full tw-object-cover"
/>
) : (
<span aria-hidden="true">{initials || "U"}</span>
)}
</button>

{open && userId && (
<div
role="menu"
aria-label="Account menu"
className="tw-absolute tw-right-0 tw-top-full tw-z-[60] tw-mt-2 tw-w-56 tw-overflow-hidden tw-rounded-lg tw-border tw-border-navy/10 tw-bg-white tw-shadow-xl tw-shadow-black/10"
>
<div className="tw-border-b tw-border-navy/10 tw-px-4 tw-py-3">
<p className="tw-mb-0 tw-truncate tw-font-mono tw-text-xs tw-uppercase tw-tracking-widest tw-text-navy/60">
Signed in as
</p>
<p className="tw-mb-0 tw-truncate tw-text-sm tw-font-bold tw-text-navy">
{name}
</p>
</div>
<Link
href={`/profile/${userId}`}
role="menuitem"
onClick={() => setOpen(false)}
className="tw-block tw-px-4 tw-py-2.5 tw-font-mono tw-text-sm tw-text-navy tw-transition-colors hover:tw-bg-navy/5 hover:tw-text-secondary"
>
<i className="fas fa-user tw-mr-2 tw-text-navy/50" />
My Profile
</Link>
<Link
href={`/profile/${userId}?tab=settings`}
role="menuitem"
onClick={() => setOpen(false)}
className="tw-block tw-px-4 tw-py-2.5 tw-font-mono tw-text-sm tw-text-navy tw-transition-colors hover:tw-bg-navy/5 hover:tw-text-secondary"
>
<i className="fas fa-cog tw-mr-2 tw-text-navy/50" />
Settings
</Link>
<button
type="button"
role="menuitem"
onClick={handleLogout}
className="tw-block tw-w-full tw-border-t tw-border-navy/10 tw-px-4 tw-py-2.5 tw-text-left tw-font-mono tw-text-sm tw-text-red tw-transition-colors hover:tw-bg-red/5"
>
<i className="fas fa-sign-out-alt tw-mr-2" />
Logout
</button>
</div>
)}
</div>
);
};

export default UserMenu;
36 changes: 25 additions & 11 deletions src/layouts/headers/header-01.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import UserMenu from "@components/header/user-menu";
import Logo from "@components/logo";
import MainMenu from "@components/menu/main-menu";
import menu from "@data/menu";
import menu, { filterMenuByAuth } from "@data/menu";
import { useSticky } from "@hooks";
import BurgerButton from "@ui/burger-button";
import clsx from "clsx";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useEffect, useMemo, useState } from "react";

const MobileMenu = dynamic(() => import("../../components/menu/mobile-menu"), {
ssr: false,
Expand All @@ -23,6 +25,11 @@ const Header = ({ shadow, fluid, transparent, mode }: TProps) => {
const router = useRouter();
const [offcanvas, setOffcanvas] = useState(false);
const { sticky, measuredRef } = useSticky();
const { status } = useSession();
const filteredMenu = useMemo(
() => filterMenuByAuth(menu, status === "authenticated"),
[status]
);

useEffect(() => {
setOffcanvas(false);
Expand Down Expand Up @@ -62,22 +69,29 @@ const Header = ({ shadow, fluid, transparent, mode }: TProps) => {
<MainMenu
className="tw-hidden xl:tw-block"
align="center"
menu={menu}
menu={filteredMenu}
color={mode}
/>
<div className="tw-overflow-hidden md:tw-hidden">
<BurgerButton
className="tw-pl-5 xl:tw-hidden"
onClick={() => setOffcanvas(true)}
color={mode}
label="Toggle Menu"
/>
<div className="tw-flex tw-items-center tw-justify-end tw-gap-3">
<UserMenu mode={mode} />
<div className="tw-overflow-hidden md:tw-hidden">
<BurgerButton
className="tw-pl-2 xl:tw-hidden"
onClick={() => setOffcanvas(true)}
color={mode}
label="Toggle Menu"
/>
</div>
</div>
</div>
</div>
<div className="tw-h-20" />
</header>
<MobileMenu isOpen={offcanvas} onClose={() => setOffcanvas(false)} menu={menu} />
<MobileMenu
isOpen={offcanvas}
onClose={() => setOffcanvas(false)}
menu={filteredMenu}
/>
</>
);
};
Expand Down
20 changes: 15 additions & 5 deletions src/layouts/headers/header-02.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import UserMenu from "@components/header/user-menu";
import Logo from "@components/logo";
import MainMenu from "@components/menu/main-menu";
import menu, { filterMenuByAuth } from "@data/menu";
import { useSticky } from "@hooks";
import BurgerButton from "@ui/burger-button";
import clsx from "clsx";
import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { useEffect, useMemo, useState } from "react";

const MobileMenu = dynamic(() => import("../../components/menu/mobile-menu"), {
Expand Down Expand Up @@ -51,11 +52,16 @@ const Header = ({ shadow, fluid }: TProps) => {
fluid && "tw-max-w-full tw-px-3.8 3xl:tw-px-37"
)}
>
<MainMenu menu={filteredMenu} hoverStyle="B" className="tw-hidden xl:tw-block" />
<MainMenu
menu={filteredMenu}
hoverStyle="B"
className="tw-hidden xl:tw-block"
/>
<Logo variant="dark" className="tw-max-w-[120px] sm:tw-max-w-[158px]" />
<div className="tw-flex tw-items-center tw-justify-end">
<div className="tw-flex tw-items-center tw-justify-end tw-gap-3">
<UserMenu mode="dark" />
<BurgerButton
className="tw-pl-5 xl:tw-hidden"
className="tw-pl-2 xl:tw-hidden"
color="dark"
onClick={() => setOffcanvas(true)}
label="Toggle Menu"
Expand All @@ -65,7 +71,11 @@ const Header = ({ shadow, fluid }: TProps) => {
</div>
<div className="tw-h-20" />
</header>
<MobileMenu isOpen={offcanvas} onClose={() => setOffcanvas(false)} menu={filteredMenu} />
<MobileMenu
isOpen={offcanvas}
onClose={() => setOffcanvas(false)}
menu={filteredMenu}
/>
</>
);
};
Expand Down
27 changes: 23 additions & 4 deletions src/layouts/headers/header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import UserMenu from "@components/header/user-menu";
import Logo from "@components/logo";
import MainMenu from "@components/menu/main-menu";
import Social01 from "@components/socials/social-01";
Expand All @@ -7,9 +8,9 @@ import BurgerButton from "@ui/burger-button";
import Button from "@ui/button";
import CountdownTimer from "@ui/countdown-timer/layout-03";
import clsx from "clsx";
import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { useEffect, useMemo, useState } from "react";

const MobileMenu = dynamic(() => import("../../components/menu/mobile-menu"), {
Expand Down Expand Up @@ -91,14 +92,28 @@ const Header = ({ shadow, fluid }: TProps) => {
<div className="tw-flex tw-items-center tw-justify-end tw-gap-4 tw-shrink-0">
<div className="tw-hidden lg:tw-flex tw-items-center tw-gap-2">
<span className="tw-relative tw-flex tw-h-[5px] tw-w-[5px]">
<span className="tw-absolute tw-inline-flex tw-h-full tw-w-full tw-rounded-full tw-bg-red tw-opacity-75" style={{ animation: "statusBlink 2s ease-in-out infinite" }} />
<span
className="tw-absolute tw-inline-flex tw-h-full tw-w-full tw-rounded-full tw-bg-red tw-opacity-75"
style={{
animation: "statusBlink 2s ease-in-out infinite",
}}
/>
<span className="tw-relative tw-inline-flex tw-h-[5px] tw-w-[5px] tw-rounded-full tw-bg-red" />
</span>
<span style={{ fontFamily: "var(--font-mono)", fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.06em", color: "rgba(9, 31, 64, 0.5)" }}>
<span
style={{
fontFamily: "var(--font-mono)",
fontSize: "10px",
textTransform: "uppercase",
letterSpacing: "0.06em",
color: "rgba(9, 31, 64, 0.5)",
}}
>
2026 Cohort Active
</span>
</div>
<Social01 className="tw-hidden md:tw-flex md:tw-items-center" />
<UserMenu mode="dark" />
<BurgerButton
className="tw-pl-2 xl:tw-hidden"
color="dark"
Expand All @@ -111,7 +126,11 @@ const Header = ({ shadow, fluid }: TProps) => {
<div className="tw-h-20" />
</div>
</header>
<MobileMenu isOpen={offcanvas} onClose={() => setOffcanvas(false)} menu={filteredMenu} />
<MobileMenu
isOpen={offcanvas}
onClose={() => setOffcanvas(false)}
menu={filteredMenu}
/>
</>
);
};
Expand Down
Loading