forked from UKSOURCE/hailearning.edu.vn
Merge pull request 'styling ui header menu' (#13) from refactor/huy-03022026-ui-header into main
Reviewed-on: UKSOURCE/hailearning.edu.vn#13
This commit is contained in:
@@ -1,28 +1,61 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import HeaderTop from './HeaderTop';
|
import HeaderTop from './HeaderTop';
|
||||||
import HeaderBottom from './HeaderBottom';
|
import HeaderBottom from './HeaderBottom';
|
||||||
import Offcanvas from './Offcanvas';
|
import Offcanvas from './Offcanvas';
|
||||||
|
import { headerMenuService } from '@/services/header-menu.service';
|
||||||
|
import { HeaderMenu as HeaderMenuType } from '@/types/header-menu';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false);
|
const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false);
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
|
const [menuItems, setMenuItems] = useState<any[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen);
|
const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen);
|
||||||
const toggleSearch = () => setIsSearchOpen(!isSearchOpen);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderTop />
|
<HeaderTop />
|
||||||
<HeaderBottom
|
<HeaderBottom
|
||||||
onToggleOffcanvas={toggleOffcanvas}
|
onToggleOffcanvas={toggleOffcanvas}
|
||||||
onToggleSearch={toggleSearch}
|
onToggleSearch={toggleSearch}
|
||||||
|
menuItems={menuItems}
|
||||||
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Offcanvas
|
<Offcanvas
|
||||||
isOpen={isOffcanvasOpen}
|
isOpen={isOffcanvasOpen}
|
||||||
onClose={() => setIsOffcanvasOpen(false)}
|
onClose={() => setIsOffcanvasOpen(false)}
|
||||||
|
menuItems={menuItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search Popup */}
|
{/* Search Popup */}
|
||||||
|
|||||||
@@ -1,52 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import HeaderMenu from './HeaderMenu';
|
import HeaderMenu from './HeaderMenu';
|
||||||
|
import { headerMenuService } from '@/services/header-menu.service';
|
||||||
import headerData from './header.json';
|
import { HeaderMenu as HeaderMenuType } from '@/types/header-menu';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 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[]);
|
|
||||||
|
|
||||||
interface HeaderBottomProps {
|
interface HeaderBottomProps {
|
||||||
onToggleOffcanvas: () => void;
|
onToggleOffcanvas: () => void;
|
||||||
onToggleSearch: () => void;
|
onToggleSearch: () => void;
|
||||||
|
menuItems: any[];
|
||||||
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderBottom: React.FC<HeaderBottomProps> = ({ onToggleOffcanvas, onToggleSearch }) => {
|
const HeaderBottom: React.FC<HeaderBottomProps> = ({
|
||||||
|
onToggleOffcanvas,
|
||||||
|
onToggleSearch,
|
||||||
|
menuItems,
|
||||||
|
isLoading
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<header id="header-sticky" className="header-1">
|
<header id="header-sticky" className="header-1">
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
@@ -59,7 +31,7 @@ const HeaderBottom: React.FC<HeaderBottomProps> = ({ onToggleOffcanvas, onToggle
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="mean__menu-wrapper">
|
<div className="mean__menu-wrapper">
|
||||||
<HeaderMenu menuItems={menuItems} />
|
{!isLoading && <HeaderMenu menuItems={menuItems as any} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-right d-flex align-items-center mt-0">
|
<div className="header-right d-flex align-items-center mt-0">
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ interface MenuItem {
|
|||||||
interface OffcanvasProps {
|
interface OffcanvasProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
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
|
// Explicitly casting headerData to the expected structure
|
||||||
const data = headerData as {
|
const data = headerData as {
|
||||||
top: {
|
top: {
|
||||||
@@ -37,10 +38,10 @@ const Offcanvas: React.FC<OffcanvasProps> = ({ isOpen, onClose }) => {
|
|||||||
phone: string;
|
phone: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
menu: MenuItem[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { offcanvas, top, menu } = data;
|
const { offcanvas, top } = data;
|
||||||
|
const menu = menuItems;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -48,81 +48,5 @@
|
|||||||
"workingHours": "Mod-Friday, 09am - 05pm",
|
"workingHours": "Mod-Friday, 09am - 05pm",
|
||||||
"phone": "+09 378 357 5222"
|
"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": "/blog"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Contact Us",
|
|
||||||
"href": "/contact"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
20
lib/axios.ts
Normal file
20
lib/axios.ts
Normal 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;
|
||||||
@@ -9777,3 +9777,4 @@ html.lenis body {
|
|||||||
a {
|
a {
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
} /*# sourceMappingURL=main.css.map */
|
} /*# sourceMappingURL=main.css.map */
|
||||||
|
|
||||||
|
|||||||
20
services/header-menu.service.ts
Normal file
20
services/header-menu.service.ts
Normal 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
7
types/header-menu.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface HeaderMenu {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
type?: 'internal' | 'external';
|
||||||
|
children?: HeaderMenu[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user