298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { ChevronDown, Menu, X, Globe } from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import { NavigationItem } from './NavigationItem';
|
|
import { MobileMenu } from './MobileMenu';
|
|
import { DropdownMenu } from './DropdownMenu';
|
|
|
|
type Lang = 'zh' | 'zh-tw' | 'en';
|
|
|
|
interface NavigationProps {
|
|
currentLang: Lang;
|
|
onLanguageChange: React.Dispatch<React.SetStateAction<Lang>> | ((lang: Lang) => void);
|
|
translations: any;
|
|
}
|
|
|
|
export function Navigation({ currentLang, onLanguageChange, translations }: NavigationProps) {
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
|
const [currentPath, setCurrentPath] = useState('/');
|
|
const dropdownTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const t = translations[currentLang];
|
|
|
|
// Navigation structure with dropdown menus
|
|
const navigationItems = [
|
|
{
|
|
key: 'home',
|
|
label: t.nav.home,
|
|
href: '/',
|
|
current: currentPath === '/',
|
|
},
|
|
{
|
|
key: 'products',
|
|
label: t.nav.products,
|
|
href: '/products',
|
|
current: currentPath.startsWith('/products'),
|
|
dropdown: [
|
|
{
|
|
label:
|
|
currentLang === 'zh'
|
|
? '云服务器 ECS'
|
|
: currentLang === 'zh-tw'
|
|
? '雲伺服器 ECS'
|
|
: 'Cloud Server ECS',
|
|
href: '/products/ecs',
|
|
},
|
|
{
|
|
label:
|
|
currentLang === 'zh'
|
|
? '云数据库 RDS'
|
|
: currentLang === 'zh-tw'
|
|
? '雲資料庫 RDS'
|
|
: 'Cloud Database RDS',
|
|
href: '/products/rds',
|
|
},
|
|
{
|
|
label:
|
|
currentLang === 'zh'
|
|
? '负载均衡 SLB'
|
|
: currentLang === 'zh-tw'
|
|
? '負載平衡 SLB'
|
|
: 'Load Balancer SLB',
|
|
href: '/products/slb',
|
|
},
|
|
{
|
|
label:
|
|
currentLang === 'zh'
|
|
? '对象存储 OSS'
|
|
: currentLang === 'zh-tw'
|
|
? '物件儲存 OSS'
|
|
: 'Object Storage OSS',
|
|
href: '/products/oss',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'solutions',
|
|
label: t.nav.solutions,
|
|
href: '/solutions',
|
|
current: currentPath.startsWith('/solutions'),
|
|
dropdown: [
|
|
{
|
|
label:
|
|
currentLang === 'zh'
|
|
? '企业上云'
|
|
: currentLang === 'zh-tw'
|
|
? '企業上雲'
|
|
: 'Enterprise Cloud',
|
|
href: '/solutions/enterprise',
|
|
},
|
|
{
|
|
label:
|
|
currentLang === 'zh'
|
|
? '数据备份'
|
|
: currentLang === 'zh-tw'
|
|
? '資料備份'
|
|
: 'Data Backup',
|
|
href: '/solutions/backup',
|
|
},
|
|
{
|
|
label:
|
|
currentLang === 'zh'
|
|
? '灾难恢复'
|
|
: currentLang === 'zh-tw'
|
|
? '災難恢復'
|
|
: 'Disaster Recovery',
|
|
href: '/solutions/recovery',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'support',
|
|
label: t.nav.support,
|
|
href: '/support',
|
|
current: currentPath.startsWith('/support'),
|
|
},
|
|
{
|
|
key: 'news',
|
|
label: t.nav.news,
|
|
href: '/news',
|
|
current: currentPath.startsWith('/news'),
|
|
},
|
|
{
|
|
key: 'contact',
|
|
label: t.nav.contact,
|
|
href: '/contact',
|
|
current: currentPath === '/contact',
|
|
},
|
|
];
|
|
|
|
// Handle dropdown interactions
|
|
const handleDropdownEnter = (key: string) => {
|
|
if (dropdownTimeoutRef.current) {
|
|
clearTimeout(dropdownTimeoutRef.current);
|
|
}
|
|
setActiveDropdown(key);
|
|
};
|
|
|
|
const handleDropdownLeave = () => {
|
|
dropdownTimeoutRef.current = setTimeout(() => {
|
|
setActiveDropdown(null);
|
|
}, 150);
|
|
};
|
|
|
|
// Handle keyboard navigation
|
|
const handleKeyDown = (event: React.KeyboardEvent, key: string, hasDropdown: boolean) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
if (hasDropdown) {
|
|
setActiveDropdown(activeDropdown === key ? null : key);
|
|
}
|
|
} else if (event.key === 'Escape') {
|
|
setActiveDropdown(null);
|
|
}
|
|
};
|
|
|
|
// Close mobile menu when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (isMobileMenuOpen && !(event.target as Element).closest('[data-mobile-menu]')) {
|
|
setIsMobileMenuOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('click', handleClickOutside);
|
|
return () => document.removeEventListener('click', handleClickOutside);
|
|
}, [isMobileMenuOpen]);
|
|
|
|
// Prevent body scroll when mobile menu is open
|
|
useEffect(() => {
|
|
if (isMobileMenuOpen) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = 'unset';
|
|
}
|
|
|
|
return () => {
|
|
document.body.style.overflow = 'unset';
|
|
};
|
|
}, [isMobileMenuOpen]);
|
|
|
|
return (
|
|
<nav
|
|
className="bg-gradient-to-r from-amber-900 to-yellow-800 shadow-lg sticky top-0 z-50"
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center h-16">
|
|
{/* Logo */}
|
|
<div className="flex items-center">
|
|
<Link
|
|
href="/"
|
|
className="text-2xl font-bold text-yellow-300 hover:text-yellow-200 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-2 focus:ring-offset-amber-900 rounded-md px-2 py-1"
|
|
aria-label="CloudPro - Home"
|
|
>
|
|
CloudPro
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Desktop Navigation */}
|
|
<div className="hidden lg:block">
|
|
<div className="ml-10 flex items-baseline space-x-2">
|
|
{navigationItems.map((item) => (
|
|
<div
|
|
key={item.key}
|
|
className="relative"
|
|
onMouseEnter={() =>
|
|
item.dropdown && handleDropdownEnter(item.key)
|
|
}
|
|
onMouseLeave={() => item.dropdown && handleDropdownLeave()}
|
|
>
|
|
<NavigationItem
|
|
label={item.label}
|
|
href={item.href}
|
|
current={item.current}
|
|
hasDropdown={!!item.dropdown}
|
|
isDropdownOpen={activeDropdown === item.key}
|
|
onKeyDown={(e) =>
|
|
handleKeyDown(e, item.key, !!item.dropdown)
|
|
}
|
|
/>
|
|
|
|
{item.dropdown && activeDropdown === item.key && (
|
|
<DropdownMenu
|
|
items={item.dropdown}
|
|
onClose={() => setActiveDropdown(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Language Selector & Mobile Menu Button */}
|
|
<div className="flex items-center space-x-4">
|
|
{/* Language Selector */}
|
|
<div className="relative">
|
|
<div className="relative group">
|
|
<select
|
|
value={currentLang}
|
|
onChange={(e) =>
|
|
(onLanguageChange as (lang: Lang) => void)(
|
|
e.target.value as Lang,
|
|
)
|
|
}
|
|
className="appearance-none bg-gradient-to-r from-amber-800 to-amber-700 text-yellow-100 border border-yellow-600/50 rounded-lg px-4 py-2.5 pr-10 text-sm font-medium hover:from-amber-700 hover:to-amber-600 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-2 focus:ring-offset-amber-900 transition-all duration-200 shadow-md hover:shadow-lg cursor-pointer min-w-[100px]"
|
|
aria-label="Select language"
|
|
>
|
|
<option value="zh" className="bg-amber-800 text-yellow-100">
|
|
简体中文
|
|
</option>
|
|
<option value="zh-tw" className="bg-amber-800 text-yellow-100">
|
|
繁體中文
|
|
</option>
|
|
<option value="en" className="bg-amber-800 text-yellow-100">
|
|
English
|
|
</option>
|
|
</select>
|
|
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none">
|
|
<Globe className="w-4 h-4 text-yellow-300 mr-1" />
|
|
|
|
<ChevronDown className="w-3 h-3 text-yellow-300 group-hover:text-yellow-200 transition-colors duration-200" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Menu Button */}
|
|
<button
|
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
className="lg:hidden inline-flex items-center justify-center p-2 rounded-md text-yellow-100 hover:text-yellow-300 hover:bg-amber-800 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-2 focus:ring-offset-amber-900 transition-colors duration-200"
|
|
aria-expanded={isMobileMenuOpen}
|
|
aria-label="Toggle mobile menu"
|
|
data-mobile-menu
|
|
>
|
|
{isMobileMenuOpen ? (
|
|
<X className="block h-6 w-6" aria-hidden="true" />
|
|
) : (
|
|
<Menu className="block h-6 w-6" aria-hidden="true" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Menu */}
|
|
<MobileMenu
|
|
isOpen={isMobileMenuOpen}
|
|
items={navigationItems}
|
|
onClose={() => setIsMobileMenuOpen(false)}
|
|
currentLang={currentLang}
|
|
/>
|
|
</nav>
|
|
);
|
|
}
|