diff --git a/package.json b/package.json index 5c4cd20..9a55518 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/app/[locale]/globals.css b/src/app/[locale]/globals.css index 90fa906..c06aa27 100644 --- a/src/app/[locale]/globals.css +++ b/src/app/[locale]/globals.css @@ -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 { diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index eed41ac..1d2ba36 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -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"; @@ -11,7 +10,7 @@ const sora = Sora({ subsets: ["latin"] }); type Props = { params: Promise<{ - locale: string; + locale: "en" | "id"; }>; children: React.ReactNode; }; @@ -40,7 +39,7 @@ export default async function LocaleRootLayout(props: Readonly) { const { children } = props; - if (!locales.includes(locale as any)) notFound(); + if (!locales.includes(locale)) notFound(); setRequestLocale(locale); diff --git a/src/components/common/ThemeToggle/index.tsx b/src/components/common/ThemeToggle/index.tsx index 508618c..ed06046 100644 --- a/src/components/common/ThemeToggle/index.tsx +++ b/src/components/common/ThemeToggle/index.tsx @@ -17,8 +17,8 @@ export function ThemeToggle() { return ( ); diff --git a/src/components/hooks/UseAuthUser/index.tsx b/src/components/hooks/UseAuthUser/index.tsx new file mode 100644 index 0000000..3e723f2 --- /dev/null +++ b/src/components/hooks/UseAuthUser/index.tsx @@ -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; +}; diff --git a/src/components/layout/Navbar/UserMenu.tsx b/src/components/layout/Navbar/UserMenu.tsx new file mode 100644 index 0000000..4028d9a --- /dev/null +++ b/src/components/layout/Navbar/UserMenu.tsx @@ -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 | undefined) => string; + logout: () => void; +} + +const DesktopUserMenu = ({ user, isAuthenticated, logout, t }: UserMenuProps) => { + return !isAuthenticated ? ( + + ) : ( + + + + + + + +
+ +
+
+ {user?.username} + {user?.email} +
+ +
+ + + + {t("navbar.sign-out")} + + +
+
+ ); +}; + +const MobileUserMenu = ({ user, isAuthenticated, logout, t }: UserMenuProps) => { + return ( +
+ {!isAuthenticated ? ( + + ) : ( +
+ +
+ +
+
+

{user?.username}

+

{user?.email}

+
+ + +
+ )} +
+ ); +}; + +export { DesktopUserMenu, MobileUserMenu }; diff --git a/src/components/layout/Navbar/index.tsx b/src/components/layout/Navbar/index.tsx index 9f35f7e..3d487c6 100644 --- a/src/components/layout/Navbar/index.tsx +++ b/src/components/layout/Navbar/index.tsx @@ -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(false); + return (
{/* */}
-
-
+
+
-
+ HMC Light + HMC Dark HMC Light HMC Dark + {/* Desktop Menu */} -
- + +
+ + + + {/* Mobile Menu Trigger */} + +
+
+ + {/* Mobile Menu */} +
+
+ + + {/* Mobile User Menu */} +
diff --git a/src/components/layout/Sidebar/index.tsx b/src/components/layout/Sidebar/index.tsx deleted file mode 100644 index bba7966..0000000 --- a/src/components/layout/Sidebar/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; -import { FC, useState } from "react"; -import { Menu } from "lucide-react"; -import Link from "next/link"; -import { useTranslations } from "next-intl"; - -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/Sheet"; -import { ThemeToggle } from "@/components/common/ThemeToggle"; -import LocaleToggle from "@/components/common/LocaleToggle"; - -import NavLink from "../../../lib/navigation/NavLink"; -import { LINKS } from "../../layout/Navbar/constant"; - -const Sidebar: FC = () => { - const t = useTranslations("Layout"); - const [isOpen, setIsOpen] = useState(false); - - return ( - - - - - - - - setIsOpen(false)}> -
- -
- - - -
-
- - ); -}; -export default Sidebar; diff --git a/src/components/layout/WrapperLayout/index.tsx b/src/components/layout/WrapperLayout/index.tsx index ff6b385..ed55bd8 100644 --- a/src/components/layout/WrapperLayout/index.tsx +++ b/src/components/layout/WrapperLayout/index.tsx @@ -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 }) => { @@ -12,17 +13,19 @@ const WrapperLayout = ({ children }: { children: React.ReactNode }) => { const isCertificateDetailPage = !!params?.slug && pathname.includes("certificates"); return ( - - {isCertificateDetailPage || isAuthPage ? ( - children - ) : ( - <> - - {children} -