diff --git a/app/components/layout/Header/Header.tsx b/app/components/layout/Header/Header.tsx index 7b724e1..b40de99 100644 --- a/app/components/layout/Header/Header.tsx +++ b/app/components/layout/Header/Header.tsx @@ -1,39 +1,46 @@ -'use client'; +"use client"; -import { useEffect, useState, useCallback } from 'react'; -import HeaderTop from './HeaderTop'; -import HeaderBottom from './HeaderBottom'; -import Offcanvas from './Offcanvas'; -import { headerMenuService } from '@/services/header-menu.service'; -import { HeaderMenu as HeaderMenuType } from '@/types/header-menu'; +import { useEffect, useState, useCallback } from "react"; +import HeaderTop from "./HeaderTop"; +import HeaderBottom from "./HeaderBottom"; +import Offcanvas from "./Offcanvas"; +import MobileMenu from "./MobileMenu"; +import { headerMenuService } from "@/services/header-menu.service"; +import { HeaderMenu as HeaderMenuType } from "@/types/header-menu"; const Header = () => { const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false); const [menuItems, setMenuItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen); + const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen); const toggleSearch = () => setIsSearchOpen(!isSearchOpen); // Helper to adapt 'children' from API to 'submenu' for the existing components - const adaptMenu = useCallback((item: HeaderMenuType): any => ({ - label: item.title, - href: item.url, - submenu: item.children && item.children.length > 0 - ? item.children.map((child: HeaderMenuType) => adaptMenu(child)) - : undefined - }), []); + const adaptMenu = useCallback( + (item: HeaderMenuType): any => ({ + label: item.title, + href: item.url, + submenu: + item.children && item.children.length > 0 + ? item.children.map((child: HeaderMenuType) => adaptMenu(child)) + : undefined, + }), + [], + ); useEffect(() => { const fetchMenu = async () => { try { setIsLoading(true); const data = await headerMenuService.getHeaderMenu(); - const mappedData = data.map(item => adaptMenu(item)); + const mappedData = data.map((item) => adaptMenu(item)); setMenuItems(mappedData); } catch (error) { - console.error('Error fetching menu in Header:', error); + console.error("Error fetching menu in Header:", error); } finally { setIsLoading(false); } @@ -45,30 +52,28 @@ const Header = () => { return ( <> - - - setIsOffcanvasOpen(false)} - menuItems={menuItems} - /> + + setIsOffcanvasOpen(false)} menuItems={menuItems} /> + + setIsMobileMenuOpen(false)} menuItems={menuItems} /> {/* Search Popup */} -
-
setIsSearchOpen(false)} - >
+
+
setIsSearchOpen(false)}>
diff --git a/app/components/layout/Header/HeaderBottom.tsx b/app/components/layout/Header/HeaderBottom.tsx index f00867c..6fb0af7 100644 --- a/app/components/layout/Header/HeaderBottom.tsx +++ b/app/components/layout/Header/HeaderBottom.tsx @@ -1,23 +1,25 @@ -'use client'; +"use client"; -import React, { useEffect, useState } from 'react'; -import Link from 'next/link'; -import HeaderMenu from './HeaderMenu'; -import { headerMenuService } from '@/services/header-menu.service'; -import { HeaderMenu as HeaderMenuType } from '@/types/header-menu'; +import React, { useEffect, useState } from "react"; +import Link from "next/link"; +import HeaderMenu from "./HeaderMenu"; +import { headerMenuService } from "@/services/header-menu.service"; +import { HeaderMenu as HeaderMenuType } from "@/types/header-menu"; interface HeaderBottomProps { onToggleOffcanvas: () => void; + onToggleMobileMenu: () => void; onToggleSearch: () => void; menuItems: any[]; isLoading: boolean; } -const HeaderBottom: React.FC = ({ - onToggleOffcanvas, +const HeaderBottom: React.FC = ({ + onToggleOffcanvas, + onToggleMobileMenu, onToggleSearch, menuItems, - isLoading + isLoading, }) => { return (
@@ -36,21 +38,44 @@ const HeaderBottom: React.FC = ({
- + + {/* Desktop Search Icon - hidden on mobile */} + + Apply now -
-
+
+ +
+
+ + {/* Desktop Hamburger - hidden on mobile */} +
+
diff --git a/app/components/layout/Header/MobileMenu.tsx b/app/components/layout/Header/MobileMenu.tsx new file mode 100644 index 0000000..e9cc5c5 --- /dev/null +++ b/app/components/layout/Header/MobileMenu.tsx @@ -0,0 +1,112 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import Link from "next/link"; + +interface MenuItem { + label: string; + href: string; + submenu?: MenuItem[]; +} + +interface MobileMenuProps { + isOpen: boolean; + onClose: () => void; + menuItems: MenuItem[]; +} + +const MobileMenu: React.FC = ({ isOpen, onClose, menuItems }) => { + const [expandedItems, setExpandedItems] = useState<{ [key: string]: boolean }>({}); + + // Disable body scroll when menu is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + + return () => { + document.body.style.overflow = ""; + }; + }, [isOpen]); + + const toggleSubmenu = (path: string) => { + setExpandedItems((prev) => ({ + ...prev, + [path]: !prev[path], + })); + }; + + const handleLinkClick = () => { + onClose(); + }; + + // Recursive component for rendering menu items with nested submenus + const renderMenuItem = (item: MenuItem, index: number, parentPath: string = "", level: number = 0) => { + const currentPath = parentPath ? `${parentPath}-${index}` : `${index}`; + const hasSubmenu = item.submenu && item.submenu.length > 0; + const isExpanded = expandedItems[currentPath]; + + return ( +
  • +
    + {hasSubmenu ? ( + <> + {item.label} + + + ) : ( + + {item.label} + + )} +
    + + {/* Submenu with accordion animation - supports nested submenus */} + {hasSubmenu && ( +
      + {item.submenu!.map((subItem, subIndex) => + renderMenuItem(subItem, subIndex, currentPath, level + 1), + )} +
    + )} +
  • + ); + }; + + return ( + <> + {/* Mobile Menu Slide Panel */} +
    +
    +
    + + logo + +
    + +
    + + +
    + + {/* Overlay */} +
    + + ); +}; + +export default MobileMenu; diff --git a/app/components/layout/Header/header-responsive.css b/app/components/layout/Header/header-responsive.css new file mode 100644 index 0000000..c36c59a --- /dev/null +++ b/app/components/layout/Header/header-responsive.css @@ -0,0 +1,271 @@ +/* ======================================== + HEADER RESPONSIVE FIXES + Additional responsive adjustments for header + ======================================== */ + +/* ======================================== + MOBILE & TABLET (max-width: 991px) + ======================================== */ + +@media (max-width: 991px) { + /* Ensure header is always visible and sticky */ + #header-sticky { + position: sticky !important; + top: 0; + z-index: 999; + background-color: var(--white); + } + + /* Add shadow when scrolled */ + #header-sticky.sticky { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } + + /* Header container adjustments */ + .header-1 .container-fluid { + padding-left: 20px; + padding-right: 20px; + } + + /* Header main layout */ + .header-1 .header-main { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + min-height: 60px; + } + + /* Logo section */ + .header-1 .header-left { + flex: 0 0 auto; + } + + .header-1 .header-left .logo { + margin: 0; + } + + .header-1 .header-left .logo img { + max-height: 40px; + height: 40px; + width: auto; + object-fit: contain; + } + + /* Right section with icons */ + .header-1 .header-right { + flex: 0 0 auto; + margin-top: 0 !important; + } + + .header-1 .header-right .header-call-item { + display: flex; + align-items: center; + gap: 12px; + } + + /* Search icon mobile styling */ + .header-1 .header-right .main-header__search { + width: 40px; + height: 40px; + border: 2px solid var(--theme); + border-radius: 50%; + background: transparent; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: all 0.3s ease; + } + + .header-1 .header-right .main-header__search i { + font-size: 16px; + color: var(--theme); + transition: color 0.3s ease; + } + + .header-1 .header-right .main-header__search:hover { + background-color: var(--theme); + } + + .header-1 .header-right .main-header__search:hover i { + color: var(--white); + } + + /* Hamburger icon mobile styling */ + .header-1 .header__hamburger { + margin: 0; + } + + .header-1 .header__hamburger .sidebar__toggle { + font-size: 24px; + color: var(--header); + padding: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.3s ease; + } + + .header-1 .header__hamburger .sidebar__toggle:hover { + color: var(--theme); + } + + /* Ensure icons don't stick to edges */ + .header-1 .header-right .header-call-item > * { + flex-shrink: 0; + } +} + +/* ======================================== + SMALL MOBILE (max-width: 480px) + ======================================== */ + +@media (max-width: 480px) { + .header-1 .container-fluid { + padding-left: 15px; + padding-right: 15px; + } + + .header-1 .header-main { + padding: 10px 0; + min-height: 55px; + } + + .header-1 .header-left .logo img { + max-height: 35px; + height: 35px; + } + + .header-1 .header-right .header-call-item { + gap: 10px; + } + + .header-1 .header-right .main-header__search { + width: 38px; + height: 38px; + } + + .header-1 .header-right .main-header__search i { + font-size: 15px; + } + + .header-1 .header__hamburger .sidebar__toggle { + font-size: 22px; + padding: 6px; + } +} + +/* ======================================== + DESKTOP (min-width: 992px) + ======================================== */ + +@media (min-width: 992px) { + /* Ensure desktop layout is not affected */ + .header-1 .header-main { + display: flex; + align-items: center; + justify-content: space-between; + } + + /* Desktop menu visible */ + .header-1 .mean__menu-wrapper { + display: block; + } + + /* Desktop button visible */ + .header-1 .header-right .theme-btn { + display: inline-flex; + } + + /* Ensure proper desktop search icon */ + .header-1 .header-right .main-header__search { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: var(--bg); + display: flex; + align-items: center; + justify-content: center; + } +} + +/* ======================================== + PREVENT LAYOUT SHIFT ON LOAD + ======================================== */ + +.header-1 { + transition: none; +} + +.header-1.sticky { + transition: box-shadow 0.3s ease; +} + +/* ======================================== + Z-INDEX MANAGEMENT + ======================================== */ + +.header-1 { + z-index: 999; +} + +.mobile-slide-menu { + z-index: 99999; +} + +.mobile-menu-overlay { + z-index: 99998; +} + +.offcanvas__info { + z-index: 9999999; +} + +.offcanvas__overlay { + z-index: 900; +} + +/* ======================================== + ACCESSIBILITY IMPROVEMENTS + ======================================== */ + +@media (max-width: 991px) { + /* Focus states for mobile */ + .mobile-search-icon:focus, + .mobile-hamburger .sidebar__toggle:focus { + outline: 2px solid var(--theme); + outline-offset: 2px; + } + + /* Touch target sizes */ + .mobile-search-icon, + .mobile-hamburger .sidebar__toggle { + min-width: 44px; + min-height: 44px; + } +} + +/* ======================================== + HIDE OFFCANVAS ON MOBILE (Prevent Menu Overlap) + ======================================== */ + +@media (max-width: 991px) { + /* Hide the old offcanvas menu on mobile - only use new mobile menu */ + .offcanvas__info, + .offcanvas__overlay { + display: none !important; + visibility: hidden !important; + pointer-events: none !important; + } +} + +@media (min-width: 992px) { + /* Hide mobile menu on desktop - only use offcanvas */ + .mobile-slide-menu, + .mobile-menu-overlay { + display: none !important; + visibility: hidden !important; + pointer-events: none !important; + } +} diff --git a/app/components/layout/Header/mobile-menu.css b/app/components/layout/Header/mobile-menu.css new file mode 100644 index 0000000..dd22455 --- /dev/null +++ b/app/components/layout/Header/mobile-menu.css @@ -0,0 +1,495 @@ +/* ======================================== + MOBILE HEADER & MENU STYLES + Only applies to mobile & tablet (max-width: 991px) + Desktop layout remains unchanged + ======================================== */ + +/* ======================================== + 1. MOBILE HEADER ADJUSTMENTS + ======================================== */ + +@media (max-width: 991px) { + /* Make header sticky on mobile */ + .header-1 { + position: sticky; + top: 0; + z-index: 999; + background-color: var(--white); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } + + /* Hide header top section on mobile */ + .header-top-section { + display: none; + } + + /* Mobile header adjustments */ + .header-1 .header-main { + padding: 15px 0; + } + + /* Logo adjustments for mobile */ + .header-1 .header-left .logo img { + max-height: 40px; + width: auto; + object-fit: contain; + } + + /* Hide desktop menu on mobile */ + .header-1 .mean__menu-wrapper { + display: none; + } + + /* Mobile header right section */ + .header-1 .header-right .header-call-item { + display: flex; + align-items: center; + gap: 15px; + } + + /* Hide "Apply now" button on mobile */ + .header-1 .header-right .theme-btn { + display: none; + } + + /* Mobile search icon styling */ + .mobile-search-icon { + display: flex !important; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: 2px solid var(--theme); + border-radius: 50%; + background: transparent; + color: var(--theme); + font-size: 16px; + cursor: pointer; + transition: all 0.3s ease; + } + + .mobile-search-icon:hover { + background-color: var(--theme); + color: var(--white); + } + + /* Hide desktop search icon on mobile */ + .desktop-search-icon { + display: none !important; + } + + /* Mobile hamburger icon styling */ + .mobile-hamburger { + display: block !important; + } + + .mobile-hamburger .sidebar__toggle { + font-size: 24px; + color: var(--header); + padding: 5px; + } + + /* Hide desktop hamburger on mobile */ + .desktop-hamburger { + display: none !important; + } + + /* Ensure proper spacing */ + .header-1 .container-fluid { + padding: 0 20px; + } +} + +/* Desktop: Show desktop icons, hide mobile icons */ +@media (min-width: 992px) { + .mobile-search-icon, + .mobile-hamburger { + display: none !important; + } + + .desktop-search-icon, + .desktop-hamburger { + display: block !important; + } +} + +/* ======================================== + 2. MOBILE SLIDE MENU + ======================================== */ + +/* Mobile menu container */ +.mobile-slide-menu { + position: fixed; + top: 0; + right: 0; + width: 100%; + max-width: 320px; + height: 100vh; + background-color: var(--white); + z-index: 99999; + transform: translateX(100%); + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); + overflow-y: auto; + box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1); +} + +.mobile-slide-menu.mobile-menu-open { + transform: translateX(0); +} + +/* Mobile menu header */ +.mobile-menu-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.mobile-menu-logo img { + max-height: 35px; + width: auto; +} + +/* Close button */ +.mobile-menu-close { + width: 45px; + height: 45px; + border-radius: 50%; + background-color: var(--theme); + border: none; + color: var(--white); + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; +} + +.mobile-menu-close:hover { + background-color: #d32f2f; + transform: rotate(90deg); +} + +/* ======================================== + 3. MOBILE MENU NAVIGATION + ======================================== */ + +.mobile-menu-nav { + padding: 10px 0; +} + +.mobile-menu-list { + list-style: none; + margin: 0; + padding: 0; +} + +.mobile-menu-item { + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.mobile-menu-item-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + position: relative; +} + +.mobile-menu-link { + flex: 1; + font-size: 16px; + font-weight: 600; + color: var(--header); + text-decoration: none; + text-transform: capitalize; + transition: color 0.3s ease; +} + +.mobile-menu-link:hover { + color: var(--theme); +} + +/* Plus/Minus toggle button */ +.mobile-menu-toggle { + width: 30px; + height: 30px; + border: none; + background: transparent; + color: var(--theme); + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + flex-shrink: 0; + margin-left: 10px; +} + +.mobile-menu-toggle:hover { + transform: scale(1.1); +} + +.mobile-menu-toggle i { + transition: transform 0.3s ease; +} + +/* ======================================== + 4. MOBILE SUBMENU (ACCORDION) + ======================================== */ + +.mobile-submenu { + list-style: none; + margin: 0; + padding: 0; + max-height: 0; + overflow: hidden; + opacity: 0; + transition: + max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s ease, + padding 0.3s ease; + background-color: rgba(0, 0, 0, 0.02); +} + +.mobile-submenu.mobile-submenu-open { + max-height: 1000px; + opacity: 1; + padding: 10px 0; +} + +.mobile-submenu-item { + padding: 0; +} + +.mobile-submenu-link { + display: block; + padding: 12px 20px 12px 40px; + font-size: 14px; + font-weight: 500; + color: var(--header); + text-decoration: none; + transition: all 0.3s ease; + position: relative; +} + +.mobile-submenu-link::before { + content: "→"; + position: absolute; + left: 25px; + opacity: 0; + transition: all 0.3s ease; +} + +.mobile-submenu-link:hover { + color: var(--theme); + padding-left: 45px; +} + +.mobile-submenu-link:hover::before { + opacity: 1; + left: 30px; +} + +/* ======================================== + 5. MOBILE MENU OVERLAY + ======================================== */ + +.mobile-menu-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + z-index: 99998; + opacity: 0; + visibility: hidden; + transition: + opacity 0.3s ease, + visibility 0.3s ease; +} + +.mobile-menu-overlay.mobile-overlay-open { + opacity: 1; + visibility: visible; +} + +/* ======================================== + 6. RESPONSIVE ADJUSTMENTS + ======================================== */ + +/* Tablet adjustments */ +@media (max-width: 991px) and (min-width: 768px) { + .mobile-slide-menu { + max-width: 380px; + } + + .mobile-menu-link { + font-size: 17px; + } +} + +/* Small mobile devices */ +@media (max-width: 480px) { + .mobile-slide-menu { + max-width: 280px; + } + + .header-1 .container-fluid { + padding: 0 15px; + } + + .mobile-menu-header { + padding: 15px; + } + + .mobile-menu-close { + width: 40px; + height: 40px; + font-size: 16px; + } +} + +/* ======================================== + 7. PREVENT BODY SCROLL WHEN MENU OPEN + ======================================== */ + +body.mobile-menu-active { + overflow: hidden; +} + +/* ======================================== + 8. SMOOTH ANIMATIONS + ======================================== */ + +@media (prefers-reduced-motion: reduce) { + .mobile-slide-menu, + .mobile-submenu, + .mobile-menu-overlay, + .mobile-menu-toggle i { + transition: none; + } +} + +/* ======================================== + 9. DESKTOP - ENSURE NO MOBILE STYLES + ======================================== */ + +@media (min-width: 992px) { + .mobile-slide-menu, + .mobile-menu-overlay { + display: none !important; + } +} + +/* ======================================== + NESTED SUBMENU SUPPORT (Multi-level) + ======================================== */ + +/* Increase max-height for nested submenus */ +.mobile-submenu.mobile-submenu-open { + max-height: 2000px !important; +} + +/* Level 1 submenu (first level) */ +.mobile-submenu-level-1 { + background-color: rgba(0, 0, 0, 0.02); +} + +.mobile-submenu-level-1 .mobile-menu-item-wrapper { + padding-left: 40px; + padding-right: 20px; +} + +.mobile-submenu-level-1 .mobile-menu-link { + font-size: 14px; + font-weight: 500; +} + +/* Level 2 submenu (nested inside level 1) */ +.mobile-submenu-level-2 { + background-color: rgba(0, 0, 0, 0.04); +} + +.mobile-submenu-level-2 .mobile-menu-item-wrapper { + padding-left: 60px; + padding-right: 20px; +} + +.mobile-submenu-level-2 .mobile-menu-link { + font-size: 13px; + font-weight: 400; +} + +/* Level 3 submenu (nested inside level 2) */ +.mobile-submenu-level-3 { + background-color: rgba(0, 0, 0, 0.06); +} + +.mobile-submenu-level-3 .mobile-menu-item-wrapper { + padding-left: 80px; + padding-right: 20px; +} + +.mobile-submenu-level-3 .mobile-menu-link { + font-size: 12px; + font-weight: 400; +} + +/* Adjust toggle button size for nested items */ +.mobile-submenu .mobile-menu-toggle { + width: 26px; + height: 26px; + font-size: 14px; +} + +/* Border for nested items */ +.mobile-menu-level-1 { + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.mobile-menu-level-2, +.mobile-menu-level-3 { + border-bottom: none; +} + +/* Adjust padding for nested menu items */ +.mobile-menu-level-1 .mobile-menu-item-wrapper, +.mobile-menu-level-2 .mobile-menu-item-wrapper, +.mobile-menu-level-3 .mobile-menu-item-wrapper { + padding-top: 12px; + padding-bottom: 12px; +} + +/* ======================================== + PREVENT MENU OVERLAP - HIDE OLD OFFCANVAS ON MOBILE + ======================================== */ + +@media (max-width: 991px) { + /* Completely hide offcanvas menu on mobile */ + .offcanvas__info, + .offcanvas__overlay, + .fix-area { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + pointer-events: none !important; + z-index: -1 !important; + } +} + +@media (min-width: 992px) { + /* Completely hide mobile menu on desktop */ + .mobile-slide-menu, + .mobile-menu-overlay { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + pointer-events: none !important; + z-index: -1 !important; + } +} diff --git a/app/globals.css b/app/globals.css index 24d681c..22cc726 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,8 +1,15 @@ @import "tailwindcss"; + +/* Mobile Menu Styles */ +@import "./components/layout/Header/mobile-menu.css"; + .collapse { - visibility: visible !important; + visibility: visible !important; } .collapse.show { - visibility: visible; + visibility: visible; } + +/* Header Responsive Styles */ +@import "./components/layout/Header/header-responsive.css";