From c98ccd1fa1c2b45cf5c5026094de845872ec500f Mon Sep 17 00:00:00 2001 From: Le Nhut Huy Date: Thu, 5 Feb 2026 00:04:28 +0700 Subject: [PATCH] styling ui header menu --- app/components/layout/Header/Header.tsx | 35 ++++++- app/components/layout/Header/HeaderBottom.tsx | 52 +++-------- app/components/layout/Header/Offcanvas.tsx | 7 +- app/components/layout/Header/header.json | 92 +------------------ lib/axios.ts | 20 ++++ public/assets/css/main.css | 1 + services/header-menu.service.ts | 20 ++++ types/header-menu.ts | 7 ++ 8 files changed, 99 insertions(+), 135 deletions(-) create mode 100644 lib/axios.ts create mode 100644 services/header-menu.service.ts create mode 100644 types/header-menu.ts diff --git a/app/components/layout/Header/Header.tsx b/app/components/layout/Header/Header.tsx index 6ec07fa..7b724e1 100644 --- a/app/components/layout/Header/Header.tsx +++ b/app/components/layout/Header/Header.tsx @@ -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([]); + 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 ( <> setIsOffcanvasOpen(false)} + menuItems={menuItems} /> {/* Search Popup */} diff --git a/app/components/layout/Header/HeaderBottom.tsx b/app/components/layout/Header/HeaderBottom.tsx index a401863..f00867c 100644 --- a/app/components/layout/Header/HeaderBottom.tsx +++ b/app/components/layout/Header/HeaderBottom.tsx @@ -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 = ({ onToggleOffcanvas, onToggleSearch }) => { +const HeaderBottom: React.FC = ({ + onToggleOffcanvas, + onToggleSearch, + menuItems, + isLoading +}) => { return (
@@ -59,7 +31,7 @@ const HeaderBottom: React.FC = ({ onToggleOffcanvas, onToggle
- + {!isLoading && }
diff --git a/app/components/layout/Header/Offcanvas.tsx b/app/components/layout/Header/Offcanvas.tsx index e1d0bd2..b03ed6e 100644 --- a/app/components/layout/Header/Offcanvas.tsx +++ b/app/components/layout/Header/Offcanvas.tsx @@ -20,9 +20,10 @@ interface MenuItem { interface OffcanvasProps { isOpen: boolean; onClose: () => void; + menuItems: any[]; } -const Offcanvas: React.FC = ({ isOpen, onClose }) => { +const Offcanvas: React.FC = ({ isOpen, onClose, menuItems }) => { // Explicitly casting headerData to the expected structure const data = headerData as { top: { @@ -37,10 +38,10 @@ const Offcanvas: React.FC = ({ isOpen, onClose }) => { phone: string; }; }; - menu: MenuItem[]; }; - const { offcanvas, top, menu } = data; + const { offcanvas, top } = data; + const menu = menuItems; return ( <> diff --git a/app/components/layout/Header/header.json b/app/components/layout/Header/header.json index cf55fb5..ae91152 100644 --- a/app/components/layout/Header/header.json +++ b/app/components/layout/Header/header.json @@ -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" - } - ] + } } diff --git a/lib/axios.ts b/lib/axios.ts new file mode 100644 index 0000000..66d7f04 --- /dev/null +++ b/lib/axios.ts @@ -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; diff --git a/public/assets/css/main.css b/public/assets/css/main.css index 55e5e10..4c1bea0 100644 --- a/public/assets/css/main.css +++ b/public/assets/css/main.css @@ -9499,3 +9499,4 @@ html.lenis body { a { color: var(--white); } /*# sourceMappingURL=main.css.map */ + diff --git a/services/header-menu.service.ts b/services/header-menu.service.ts new file mode 100644 index 0000000..8b9b4c8 --- /dev/null +++ b/services/header-menu.service.ts @@ -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 { + 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 + } + } +}; diff --git a/types/header-menu.ts b/types/header-menu.ts new file mode 100644 index 0000000..f65352b --- /dev/null +++ b/types/header-menu.ts @@ -0,0 +1,7 @@ +export interface HeaderMenu { + id: string; + title: string; + url: string; + type?: 'internal' | 'external'; + children?: HeaderMenu[]; +}