update: layout
All checks were successful
Build and Release / release (push) Successful in 27s

This commit is contained in:
2026-04-29 16:32:46 +07:00
parent 65806d197f
commit 41af501b51
5 changed files with 128 additions and 87 deletions

View File

@@ -8,7 +8,6 @@ import { apiGetCurrentUser } from "@/service/auth";
import { setUserData } from "@/store/features/userSlice"; import { setUserData } from "@/store/features/userSlice";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { usePathname } from "next/navigation";
export default function AdminLayout({ export default function AdminLayout({
children, children,
@@ -17,8 +16,6 @@ export default function AdminLayout({
}) { }) {
const { isExpanded, isHovered, isMobileOpen } = useSidebar(); const { isExpanded, isHovered, isMobileOpen } = useSidebar();
const dispatch = useDispatch() const dispatch = useDispatch()
const pathname = usePathname();
const isHomePage = pathname === "/";
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
@@ -34,19 +31,17 @@ export default function AdminLayout({
// Dynamic class for main content margin based on sidebar state // Dynamic class for main content margin based on sidebar state
const mainContentMargin = isHomePage const mainContentMargin = isMobileOpen
? "ml-0" ? "ml-0"
: isMobileOpen : isExpanded || isHovered
? "ml-0" ? "lg:ml-[290px]"
: isExpanded || isHovered : "lg:ml-[0px]";
? "lg:ml-[290px]"
: "lg:ml-[90px]";
return ( return (
<div className="min-h-screen xl:flex"> <div className="min-h-screen xl:flex">
{/* Sidebar and Backdrop */} {/* Sidebar and Backdrop */}
{!isHomePage && <AppSidebar />} <AppSidebar />
{!isHomePage && <Backdrop />} <Backdrop />
{/* Main Content Area */} {/* Main Content Area */}
<div <div
className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`} className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`}

View File

@@ -27,7 +27,7 @@ export const useSidebar = () => {
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({
children, children,
}) => { }) => {
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(false);
const [isMobileOpen, setIsMobileOpen] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);

View File

@@ -5,15 +5,12 @@ import UserDropdown from "@/components/header/UserDropdown";
import { useSidebar } from "@/context/SidebarContext"; import { useSidebar } from "@/context/SidebarContext";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation";
import React, { useState ,useEffect,useRef} from "react"; import React, { useState ,useEffect,useRef} from "react";
const AppHeader: React.FC = () => { const AppHeader: React.FC = () => {
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false); const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
const { isMobileOpen, toggleSidebar, toggleMobileSidebar } = useSidebar(); const { isMobileOpen, toggleSidebar, toggleMobileSidebar } = useSidebar();
const pathname = usePathname();
const isHomePage = pathname === "/";
const handleToggle = () => { const handleToggle = () => {
if (window.innerWidth >= 1024) { if (window.innerWidth >= 1024) {
@@ -47,46 +44,44 @@ const AppHeader: React.FC = () => {
<header className="sticky top-0 flex w-full bg-white border-gray-200 z-99 dark:border-gray-800 dark:bg-gray-900 lg:border-b"> <header className="sticky top-0 flex w-full bg-white border-gray-200 z-99 dark:border-gray-800 dark:bg-gray-900 lg:border-b">
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6"> <div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4"> <div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
{!isHomePage && ( <button
<button className="items-center justify-center w-10 h-10 text-gray-500 border-gray-200 rounded-lg z-99999 dark:border-gray-800 lg:flex dark:text-gray-400 lg:h-11 lg:w-11 lg:border"
className="items-center justify-center w-10 h-10 text-gray-500 border-gray-200 rounded-lg z-99999 dark:border-gray-800 lg:flex dark:text-gray-400 lg:h-11 lg:w-11 lg:border" onClick={handleToggle}
onClick={handleToggle} aria-label="Toggle Sidebar"
aria-label="Toggle Sidebar" >
> {isMobileOpen ? (
{isMobileOpen ? ( <svg
<svg width="24"
width="24" height="24"
height="24" viewBox="0 0 24 24"
viewBox="0 0 24 24" fill="none"
fill="none" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" >
> <path
<path fillRule="evenodd"
fillRule="evenodd" clipRule="evenodd"
clipRule="evenodd" d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z" fill="currentColor"
fill="currentColor" />
/> </svg>
</svg> ) : (
) : ( <svg
<svg width="16"
width="16" height="12"
height="12" viewBox="0 0 16 12"
viewBox="0 0 16 12" fill="none"
fill="none" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" >
> <path
<path fillRule="evenodd"
fillRule="evenodd" clipRule="evenodd"
clipRule="evenodd" d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z" fill="currentColor"
fill="currentColor" />
/> </svg>
</svg> )}
)} {/* Cross Icon */}
{/* Cross Icon */} </button>
</button>
)}
<Link href="/" className="lg:hidden"> <Link href="/" className="lg:hidden">
<Image <Image

View File

@@ -90,23 +90,26 @@ const AppSidebar: React.FC = () => {
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar(); const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
const pathname = usePathname(); const pathname = usePathname();
const [openSubmenu, setOpenSubmenu] = useState<{ const [openSubmenu, setOpenSubmenu] = useState<{
type: "main" | "others"; type: "main" | "others";
index: number; index: number;
} | null>(null); } | null>(null);
const [subMenuHeight, setSubMenuHeight] = useState<Record<string, number>>({}); const [subMenuHeight, setSubMenuHeight] = useState<Record<string, number>>(
{},
);
const subMenuRefs = useRef<Record<string, HTMLDivElement | null>>({}); const subMenuRefs = useRef<Record<string, HTMLDivElement | null>>({});
const isActive = useCallback((path: string) => path === pathname, [pathname]); const isActive = useCallback((path: string) => path === pathname, [pathname]);
const handleSubmenuToggle = (index: number, menuType: "main" | "others") => { const handleSubmenuToggle = (index: number, menuType: "main" | "others") => {
setOpenSubmenu((prev) => setOpenSubmenu((prev) =>
prev?.type === menuType && prev?.index === index ? null : { type: menuType, index } prev?.type === menuType && prev?.index === index
? null
: { type: menuType, index },
); );
}; };
useEffect(() => { useEffect(() => {
let submenuMatched = false; let submenuMatched = false;
[ [
{ items: ALL_NAV_ITEMS, type: "main" }, { items: ALL_NAV_ITEMS, type: "main" },
@@ -151,23 +154,41 @@ const AppSidebar: React.FC = () => {
onClick={() => handleSubmenuToggle(index, menuType)} onClick={() => handleSubmenuToggle(index, menuType)}
className={`menu-item group uppercase ${ className={`menu-item group uppercase ${
openSubmenu?.type === menuType && openSubmenu?.index === index openSubmenu?.type === menuType && openSubmenu?.index === index
? "menu-item-active" : "menu-item-inactive" ? "menu-item-active"
: "menu-item-inactive"
} cursor-pointer ${!isExpanded && !isHovered ? "lg:justify-center" : "lg:justify-start"}`} } cursor-pointer ${!isExpanded && !isHovered ? "lg:justify-center" : "lg:justify-start"}`}
> >
<span className={openSubmenu?.type === menuType && openSubmenu?.index === index ? "menu-item-icon-active" : "menu-item-icon-inactive"}> <span
className={
openSubmenu?.type === menuType && openSubmenu?.index === index
? "menu-item-icon-active"
: "menu-item-icon-inactive"
}
>
{nav.icon} {nav.icon}
</span> </span>
{(isExpanded || isHovered || isMobileOpen) && ( {(isExpanded || isHovered || isMobileOpen) && (
<> <>
<span className={`menu-item-text`}>{nav.name}</span> <span className={`menu-item-text`}>{nav.name}</span>
<ChevronDownIcon className={`ml-auto w-5 h-5 transition-transform duration-200 ${openSubmenu?.type === menuType && openSubmenu?.index === index ? "rotate-180 text-brand-500" : ""}`} /> <ChevronDownIcon
className={`ml-auto w-5 h-5 transition-transform duration-200 ${openSubmenu?.type === menuType && openSubmenu?.index === index ? "rotate-180 text-brand-500" : ""}`}
/>
</> </>
)} )}
</button> </button>
) : ( ) : (
nav.path && ( nav.path && (
<Link href={nav.path} className={`menu-item group ${isActive(nav.path) ? "menu-item-active" : "menu-item-inactive"}`}> <Link
<span className={isActive(nav.path) ? "menu-item-icon-active" : "menu-item-icon-inactive"}> href={nav.path}
className={`menu-item group ${isActive(nav.path) ? "menu-item-active" : "menu-item-inactive"}`}
>
<span
className={
isActive(nav.path)
? "menu-item-icon-active"
: "menu-item-icon-inactive"
}
>
{nav.icon} {nav.icon}
</span> </span>
{(isExpanded || isHovered || isMobileOpen) && ( {(isExpanded || isHovered || isMobileOpen) && (
@@ -178,18 +199,40 @@ const AppSidebar: React.FC = () => {
)} )}
{nav.subItems && (isExpanded || isHovered || isMobileOpen) && ( {nav.subItems && (isExpanded || isHovered || isMobileOpen) && (
<div <div
ref={(el) => { subMenuRefs.current[`${menuType}-${index}`] = el; }} ref={(el) => {
subMenuRefs.current[`${menuType}-${index}`] = el;
}}
className="overflow-hidden transition-all duration-300" className="overflow-hidden transition-all duration-300"
style={{ height: openSubmenu?.type === menuType && openSubmenu?.index === index ? `${subMenuHeight[`${menuType}-${index}`]}px` : "0px" }} style={{
height:
openSubmenu?.type === menuType && openSubmenu?.index === index
? `${subMenuHeight[`${menuType}-${index}`]}px`
: "0px",
}}
> >
<ul className="mt-2 space-y-1 ml-9"> <ul className="mt-2 space-y-1 ml-9">
{nav.subItems.map((subItem) => ( {nav.subItems.map((subItem) => (
<li key={subItem.name}> <li key={subItem.name}>
<Link href={subItem.path} className={`menu-dropdown-item ${isActive(subItem.path) ? "menu-dropdown-item-active" : "menu-dropdown-item-inactive"}`}> <Link
href={subItem.path}
className={`menu-dropdown-item ${isActive(subItem.path) ? "menu-dropdown-item-active" : "menu-dropdown-item-inactive"}`}
>
{subItem.name} {subItem.name}
<span className="flex items-center gap-1 ml-auto"> <span className="flex items-center gap-1 ml-auto">
{subItem.new && <span className={`${isActive(subItem.path) ? "menu-dropdown-badge-active" : "menu-dropdown-badge-inactive"} menu-dropdown-badge`}>new</span>} {subItem.new && (
{subItem.pro && <span className={`${isActive(subItem.path) ? "menu-dropdown-badge-active" : "menu-dropdown-badge-inactive"} menu-dropdown-badge`}>pro</span>} <span
className={`${isActive(subItem.path) ? "menu-dropdown-badge-active" : "menu-dropdown-badge-inactive"} menu-dropdown-badge`}
>
new
</span>
)}
{subItem.pro && (
<span
className={`${isActive(subItem.path) ? "menu-dropdown-badge-active" : "menu-dropdown-badge-inactive"} menu-dropdown-badge`}
>
pro
</span>
)}
</span> </span>
</Link> </Link>
</li> </li>
@@ -204,13 +247,15 @@ const AppSidebar: React.FC = () => {
return ( return (
<aside <aside
className={`fixed mt-16 flex flex-col lg:mt-0 top-0 px-5 left-0 bg-white dark:bg-gray-900 dark:border-gray-800 text-gray-900 h-screen transition-all duration-300 ease-in-out z-50 border-r border-gray-200 className={`fixed mt-16 flex flex-col lg:mt-0 top-0 left-0 bg-white dark:bg-gray-900 dark:border-gray-800 text-gray-900 h-screen transition-all duration-300 ease-in-out z-50 border-r border-gray-200
${isExpanded || isMobileOpen ? "w-[290px]" : isHovered ? "w-[290px]" : "w-[90px]"} ${isExpanded || isMobileOpen || isHovered ? "w-[290px] px-5" : "w-0 px-0 border-none overflow-hidden"}
${isMobileOpen ? "translate-x-0" : "-translate-x-full"} lg:translate-x-0`} ${isMobileOpen ? "translate-x-0" : "-translate-x-full"} lg:translate-x-0`}
onMouseEnter={() => !isExpanded && setIsHovered(true)} onMouseEnter={() => !isExpanded && setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<div className={`py-8 flex ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}> <div
className={`py-8 flex ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}
>
<Link href="/"> <Link href="/">
<Image <Image
src="/images/logo/logo.svg" src="/images/logo/logo.svg"
@@ -224,8 +269,14 @@ const AppSidebar: React.FC = () => {
<nav className="mb-6"> <nav className="mb-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<h2 className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}> <h2
{isExpanded || isHovered || isMobileOpen ? "Menu" : <HorizontaLDots />} className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}
>
{isExpanded || isHovered || isMobileOpen ? (
"Menu"
) : (
<HorizontaLDots />
)}
</h2> </h2>
{renderMenuItems(ALL_NAV_ITEMS, "main")} {renderMenuItems(ALL_NAV_ITEMS, "main")}
</div> </div>