Skip to content

[FEAT] - Revamp Navbar and Show User on Navbar #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 25, 2025
Merged
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.2.2",
Expand All @@ -36,6 +36,7 @@
"clsx": "^2.1.1",
"embla-carousel-autoplay": "^8.2.0",
"embla-carousel-react": "^8.2.0",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.511.0",
"motion": "^12.7.4",
"next": "15.3.2",
Expand Down
13 changes: 13 additions & 0 deletions src/app/[locale]/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,19 @@
body {
@apply bg-background text-foreground;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
box-shadow: 0 0 0px 1000px transparent inset !important;
-webkit-text-fill-color: var(--color-foreground);
transition: background-color 5000s ease-in-out 0s;
}
}

@utility container {
Expand Down
5 changes: 2 additions & 3 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server";
import { Sora } from "next/font/google";
Expand All @@ -11,7 +10,7 @@ const sora = Sora({ subsets: ["latin"] });

type Props = {
params: Promise<{
locale: string;
locale: "en" | "id";
}>;
children: React.ReactNode;
};
Expand Down Expand Up @@ -40,7 +39,7 @@ export default async function LocaleRootLayout(props: Readonly<Props>) {

const { children } = props;

if (!locales.includes(locale as any)) notFound();
if (!locales.includes(locale)) notFound();

setRequestLocale(locale);

Expand Down
4 changes: 2 additions & 2 deletions src/components/common/ThemeToggle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export function ThemeToggle() {

return (
<Button onClick={handleSetTheme} size="sm" className="w-10">
<Sun className="h-4 w-4 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-4 w-4 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<Sun size={16} className="scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon size={16} className="absolute scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
);
Expand Down
9 changes: 9 additions & 0 deletions src/components/hooks/UseAuthUser/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useContext } from "react";
import { UserContext } from "@/components/provider/AuthProvider";
import { UserContextType } from "@/types";

export const useAuthUser = (): UserContextType => {
const context = useContext(UserContext);
if (!context) throw new Error("useAuthUser must be used within an AuthProvider");
return context;
};
88 changes: 88 additions & 0 deletions src/components/layout/Navbar/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Button } from "@/components/ui/Button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/DropdownMenu";
import { Link } from "@/lib/navigation";
import { User as UserType } from "@/types";
import { ChevronDown, User, X } from "lucide-react";

interface UserMenuProps {
user: UserType | null;
isAuthenticated: boolean;
t: (key: string, values?: Record<string, string | number | Date> | undefined) => string;
logout: () => void;
}

const DesktopUserMenu = ({ user, isAuthenticated, logout, t }: UserMenuProps) => {
return !isAuthenticated ? (
<Button asChild size="sm" className="w-full">
<Link href="/sign-in">{t("navbar.sign-in")}</Link>
</Button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="flex h-auto min-w-36 items-center gap-3 px-3 py-2.5">
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-full p-1">
<User size={14} />
</div>
<div className="flex flex-col items-start text-left">
<span className="text-sm leading-none font-medium">{user?.username}</span>
</div>
<ChevronDown size={16} className="ml-auto" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-background space-y-1">
<DropdownMenuItem asChild>
<Link href="/profile" className="flex items-center gap-3 p-3">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-full p-1">
<User size={18} />
</div>
<div className="flex flex-col">
<span className="text-sm font-medium">{user?.username}</span>
<span className="text-muted-foreground text-xs">{user?.email}</span>
</div>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={logout}>
<span className="text-destructive hover:text-destructive/80 flex cursor-pointer items-center gap-2">
<X size={16} />
{t("navbar.sign-out")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

const MobileUserMenu = ({ user, isAuthenticated, logout, t }: UserMenuProps) => {
return (
<div className="border-t pt-4">
{!isAuthenticated ? (
<Button asChild size="sm" className="w-full">
<Link href="/sign-in">{t("navbar.sign-in")}</Link>
</Button>
) : (
<div className="space-y-3">
<Link href="/profile" className="bg-muted/50 hover:bg-muted flex items-center gap-3 rounded-lg p-3">
<div className="bg-primary text-primary-foreground flex h-8 w-8 items-center justify-center rounded-full">
<User size={16} />
</div>
<div className="flex-1">
<p className="text-sm font-medium">{user?.username}</p>
<p className="text-muted-foreground text-xs">{user?.email}</p>
</div>
</Link>
<Button
variant="outline"
size="sm"
className="text-destructive hover:text-destructive/80 w-full cursor-pointer justify-start"
onClick={logout}
>
<X size={16} className="mr-2" />
{t("navbar.sign-out")}
</Button>
</div>
)}
</div>
);
};

export { DesktopUserMenu, MobileUserMenu };
89 changes: 74 additions & 15 deletions src/components/layout/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,110 @@
"use client";

import { useState } from "react";
import { useTranslations } from "next-intl";
import { Menu, X } from "lucide-react";
import Image from "next/image";
import { Link } from "@/lib/navigation";
import { Button } from "@/components/ui/Button";
import { useAuthUser } from "@/components/hooks/UseAuthUser";
import { LINKS } from "./constant";
import LocaleToggle from "../../common/LocaleToggle";
import NavLink from "../../../lib/navigation/NavLink";
import { ThemeToggle } from "../../common/ThemeToggle";
import { useTranslations } from "next-intl";
import LocaleToggle from "../../common/LocaleToggle";
import { LINKS } from "./constant";

import { useAuth } from "@/features/auth/hooks/useAuth";
import { DesktopUserMenu, MobileUserMenu } from "./UserMenu";
// import AnnouncementLayout from "@/components/layout/AnnouncementLayout";
import Sidebar from "../Sidebar";
import Image from "next/image";

const Navbar = () => {
const t = useTranslations("Layout");
const { user, isAuthenticated } = useAuthUser();
const { logout } = useAuth();

const [isOpen, setIsOpen] = useState<boolean>(false);

return (
<header className="fixed top-0 z-50 w-full">
{/* <AnnouncementLayout /> */}
<div className="border-b bg-white dark:bg-slate-950">
<div className="container mx-auto py-5">
<div className="flex items-center justify-between gap-4">
<div className="container mx-auto px-5 py-5">
<div className="flex flex-wrap items-center justify-between gap-4">
<Link href="/" className="flex items-center gap-2">
<div className="h-8 w-10 bg-[url('/assets/icons/ic_hmc-light.svg')] bg-cover bg-center dark:bg-[url('/assets/icons/ic_hmc-dark.svg')]"></div>
<Image
src="/assets/icons/ic_hmc-light.svg"
alt="HMC Light"
width={40}
height={40}
className="w-7 md:w-10 dark:hidden"
/>
<Image
src="/assets/icons/ic_hmc-dark.svg"
alt="HMC Dark"
width={40}
height={40}
className="hidden w-7 md:w-10 dark:inline"
/>
<Image
src="/assets/icons/ic_hmc-text-light.svg"
alt="HMC Light"
width={160}
height={18}
className="light:inline w-32 transition-opacity duration-300 lg:w-40 dark:hidden"
className="inline w-28 sm:w-32 lg:w-40 dark:hidden"
/>
<Image
src="/assets/icons/ic_hmc-text-dark.svg"
alt="HMC Dark"
width={160}
height={18}
className="hidden w-32 transition-opacity duration-300 lg:w-40 dark:inline"
className="hidden w-28 sm:w-32 lg:w-40 dark:inline"
/>
</Link>

{/* Desktop Menu */}
<nav className="hidden items-center gap-7 lg:flex">
{LINKS.map(({ href, id }) => (
<NavLink key={id} href={href} title={t(`navbar.link-${id}`)} />
))}
<div className="flex items-center gap-2">
<ThemeToggle />

<div className="flex items-center gap-3">
<LocaleToggle />
<ThemeToggle />

{/* Desktop User Menu */}
<DesktopUserMenu user={user} isAuthenticated={isAuthenticated} logout={logout} t={t} />
</div>
</nav>
<div className="flex lg:hidden">
<Sidebar />

<div className="flex items-center gap-2 lg:hidden">
<LocaleToggle />
<ThemeToggle />

{/* Mobile Menu Trigger */}
<Button
className="p-2"
onClick={() => setIsOpen(!isOpen)}
aria-label={isOpen ? "Close menu" : "Open menu"}
aria-expanded={isOpen}
>
{isOpen ? <X /> : <Menu />}
</Button>
</div>
</div>

{/* Mobile Menu */}
<div
className={`transition-all duration-300 ease-in-out lg:hidden ${
isOpen ? "max-h-96 opacity-100" : "max-h-0 overflow-hidden py-0 opacity-0"
}`}
>
<div className="mx-auto mt-5 w-full space-y-6">
<nav className="flex flex-col gap-7">
{LINKS.map(({ href, id }) => (
<NavLink key={id} href={href} title={t(`navbar.link-${id}`)} onClick={() => setIsOpen(false)} />
))}
</nav>

{/* Mobile User Menu */}
<MobileUserMenu user={user} isAuthenticated={isAuthenticated} logout={logout} t={t} />
</div>
</div>
</div>
Expand Down
46 changes: 0 additions & 46 deletions src/components/layout/Sidebar/index.tsx

This file was deleted.

27 changes: 15 additions & 12 deletions src/components/layout/WrapperLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import Navbar from "@/components/layout/Navbar";
import { ThemeProvider } from "./ThemeProvider";
import Footer from "@/components/layout/Footer";
import { AuthProvider } from "@/components/provider/AuthProvider";
import { ThemeProvider } from "@/components/provider/ThemeProvider";
import { useParams, usePathname } from "next/navigation";

const WrapperLayout = ({ children }: { children: React.ReactNode }) => {
Expand All @@ -12,17 +13,19 @@ const WrapperLayout = ({ children }: { children: React.ReactNode }) => {
const isCertificateDetailPage = !!params?.slug && pathname.includes("certificates");

return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{isCertificateDetailPage || isAuthPage ? (
children
) : (
<>
<Navbar />
{children}
<Footer />
</>
)}
</ThemeProvider>
<AuthProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{isCertificateDetailPage || isAuthPage ? (
children
) : (
<>
<Navbar />
{children}
<Footer />
</>
)}
</ThemeProvider>
</AuthProvider>
);
};
export default WrapperLayout;
Loading