styling ui header menu

This commit is contained in:
2026-02-05 00:04:28 +07:00
parent 00ba842b80
commit c98ccd1fa1
8 changed files with 99 additions and 135 deletions

View File

@@ -1,28 +1,61 @@
'use client';
import { useState } from 'react';
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';
const Header = () => {
const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [menuItems, setMenuItems] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen);
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
}), []);
useEffect(() => {
const fetchMenu = async () => {
try {
setIsLoading(true);
const data = await headerMenuService.getHeaderMenu();
const mappedData = data.map(item => adaptMenu(item));
setMenuItems(mappedData);
} catch (error) {
console.error('Error fetching menu in Header:', error);
} finally {
setIsLoading(false);
}
};
fetchMenu();
}, [adaptMenu]);
return (
<>
<HeaderTop />
<HeaderBottom
onToggleOffcanvas={toggleOffcanvas}
onToggleSearch={toggleSearch}
menuItems={menuItems}
isLoading={isLoading}
/>
<Offcanvas
isOpen={isOffcanvasOpen}
onClose={() => setIsOffcanvasOpen(false)}
menuItems={menuItems}
/>
{/* Search Popup */}

View File

@@ -1,52 +1,24 @@
'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import HeaderMenu from './HeaderMenu';
import headerData from './header.json';
// Map the JSON data to satisfy the HeaderMenu props interface
interface JsonMenuItem {
label: string;
href: string;
children?: JsonMenuItem[];
}
interface MenuItem {
label: string;
href: string;
submenu?: MenuItem[];
megaMenuContent?: React.ReactNode;
}
// We need to recursively map 'children' to 'submenu'
const mapMenuItems = (items: JsonMenuItem[]): MenuItem[] => {
return items.map(item => {
const newItem: MenuItem = {
label: item.label,
href: item.href,
};
if (item.children && item.children.length > 0) {
newItem.submenu = mapMenuItems(item.children);
}
return newItem;
});
};
const menuItems: MenuItem[] = mapMenuItems(headerData.menu as JsonMenuItem[]);
import { headerMenuService } from '@/services/header-menu.service';
import { HeaderMenu as HeaderMenuType } from '@/types/header-menu';
interface HeaderBottomProps {
onToggleOffcanvas: () => void;
onToggleSearch: () => void;
menuItems: any[];
isLoading: boolean;
}
const HeaderBottom: React.FC<HeaderBottomProps> = ({ onToggleOffcanvas, onToggleSearch }) => {
const HeaderBottom: React.FC<HeaderBottomProps> = ({
onToggleOffcanvas,
onToggleSearch,
menuItems,
isLoading
}) => {
return (
<header id="header-sticky" className="header-1">
<div className="container-fluid">
@@ -59,7 +31,7 @@ const HeaderBottom: React.FC<HeaderBottomProps> = ({ onToggleOffcanvas, onToggle
</Link>
</div>
<div className="mean__menu-wrapper">
<HeaderMenu menuItems={menuItems} />
{!isLoading && <HeaderMenu menuItems={menuItems as any} />}
</div>
</div>
<div className="header-right d-flex align-items-center mt-0">

View File

@@ -20,9 +20,10 @@ interface MenuItem {
interface OffcanvasProps {
isOpen: boolean;
onClose: () => void;
menuItems: any[];
}
const Offcanvas: React.FC<OffcanvasProps> = ({ isOpen, onClose }) => {
const Offcanvas: React.FC<OffcanvasProps> = ({ isOpen, onClose, menuItems }) => {
// Explicitly casting headerData to the expected structure
const data = headerData as {
top: {
@@ -37,10 +38,10 @@ const Offcanvas: React.FC<OffcanvasProps> = ({ isOpen, onClose }) => {
phone: string;
};
};
menu: MenuItem[];
};
const { offcanvas, top, menu } = data;
const { offcanvas, top } = data;
const menu = menuItems;
return (
<>

View File

@@ -48,95 +48,5 @@
"workingHours": "Mod-Friday, 09am - 05pm",
"phone": "+09 378 357 5222"
}
},
"menu": [
{
"label": "Home",
"href": "/",
"children": []
},
{
"label": "About Us",
"href": "/about",
"children": []
},
{
"label": "Pages",
"href": "#",
"children": [
{
"label": "Service",
"href": "/service",
"children": [
{
"label": "Service",
"href": "/service"
},
{
"label": "Service Details",
"href": "/service-details"
}
]
},
{
"label": "Country List",
"href": "/country-list",
"children": [
{
"label": "Country List",
"href": "/country-list"
},
{
"label": "Country Details",
"href": "/country-details"
}
]
},
{
"label": "Our Pricing",
"href": "/pricing"
},
{
"label": "Appointment",
"href": "/appointment"
}
]
},
{
"label": "VISA",
"href": "#",
"children": [
{
"label": "Visa List",
"href": "/visa-list"
},
{
"label": "Visa Details",
"href": "/visa-details"
}
]
},
{
"label": "Blog",
"href": "#",
"children": [
{
"label": "Blog Grid",
"href": "/blog-grid"
},
{
"label": "Blog Standard",
"href": "/blog"
},
{
"label": "Blog Details",
"href": "/blog-details"
}
]
},
{
"label": "Contact Us",
"href": "/contact"
}
]
}

20
lib/axios.ts Normal file
View File

@@ -0,0 +1,20 @@
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Response interceptor for basic error handling
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Error:', error.response?.data || error.message);
return Promise.reject(error);
}
);
export default axiosInstance;

View File

@@ -9499,3 +9499,4 @@ html.lenis body {
a {
color: var(--white);
} /*# sourceMappingURL=main.css.map */

View File

@@ -0,0 +1,20 @@
import axiosInstance from '../lib/axios';
import { HeaderMenu } from '../types/header-menu';
export const headerMenuService = {
/**
* Fetch active header menu tree from API
*/
async getHeaderMenu(): Promise<HeaderMenu[]> {
try {
const response = await axiosInstance.get<{ success: boolean; data: HeaderMenu[] }>('/api/header-menu');
if (response.data.success) {
return response.data.data;
}
return [];
} catch (error) {
console.error('Failed to fetch header menu:', error);
return []; // Fallback to empty menu
}
}
};

7
types/header-menu.ts Normal file
View File

@@ -0,0 +1,7 @@
export interface HeaderMenu {
id: string;
title: string;
url: string;
type?: 'internal' | 'external';
children?: HeaderMenu[];
}