index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import { Fragment, memo, useMemo } from 'react';
  2. import { Icon } from '@iconify/react';
  3. import { Dropdown, type MenuProps } from 'antd';
  4. import { useTranslation } from 'react-i18next';
  5. import logoUnion from '@/assets/iconify/multi-color/logo-union.svg';
  6. import chevronDownIcon from '@/assets/iconify/single-color/chevron-down.svg';
  7. import closeIcon from '@/assets/iconify/single-color/close.svg';
  8. import menuIcon from '@/assets/iconify/single-color/menu.svg';
  9. import { LanguageSwitch } from '@/components/LanguageSwitch';
  10. import { useAuth } from '@/hooks/useAuth';
  11. import { useLoginDialog } from '@/hooks/useLoginDialog';
  12. import { useResponsive } from '@/hooks/useSize';
  13. import type { NavMenuItem } from '@/utils/navUtils';
  14. import { useAction } from './useAction';
  15. import { useService } from './useService';
  16. const Topbar = memo(() => {
  17. const { t } = useTranslation();
  18. const { isMobile } = useResponsive();
  19. const { menuItems, isActive, getMenuItemLabel } = useService();
  20. const { isLoggedIn } = useAuth();
  21. const openLoginDialog = useLoginDialog();
  22. const {
  23. menuContainerRef,
  24. loginButtonRef,
  25. languageButtonRef,
  26. isMobileMenuOpen,
  27. isMobileMenuClosing,
  28. isOverflowMenuOpen,
  29. visibleMenuItems,
  30. overflowMenuItems,
  31. handleMenuClick,
  32. toggleMobileMenu,
  33. closeMobileMenu,
  34. handleMenuAnimationEnd,
  35. setOverflowMenuOpen,
  36. setMenuItemRef,
  37. } = useAction({ menuItems, isMobile, isLoggedIn });
  38. const overflowMenuProps: MenuProps = useMemo(
  39. () => ({
  40. items: overflowMenuItems.map((item: NavMenuItem) => ({
  41. key: item.name,
  42. label: getMenuItemLabel(item),
  43. onClick: () => handleMenuClick(item.path),
  44. })),
  45. }),
  46. [overflowMenuItems, getMenuItemLabel, handleMenuClick]
  47. );
  48. return (
  49. <Fragment>
  50. <header className="fixed top-0 start-0 end-0 z-50 bg-black/90 border-b border-white/10 backdrop-blur-sm">
  51. <div className="flex items-center justify-between px-5 sm:px-6 lg:px-20 py-5 max-w-[1440px] mx-auto">
  52. {/* Logo */}
  53. <div className="flex-shrink-0 flex items-center gap-3">
  54. <Icon icon={logoUnion} className="w-8 h-8" />
  55. <h1 className="text-2xl font-bold italic text-white leading-none font-[REM] tracking-wide">
  56. {t('components.topbar.logo')}
  57. </h1>
  58. </div>
  59. {/* Desktop Menu */}
  60. {!isMobile && (
  61. <nav
  62. ref={menuContainerRef}
  63. className="flex-1 flex items-center justify-end gap-2.5 ms-8 min-w-0"
  64. >
  65. <div className="flex items-center gap-2.5 min-w-0">
  66. {visibleMenuItems.map((item: NavMenuItem) => {
  67. const active = isActive(item.path);
  68. return (
  69. <button
  70. key={item.name}
  71. ref={setMenuItemRef(item.name)}
  72. onClick={() => handleMenuClick(item.path)}
  73. className={`px-2.5 h-10 flex items-center justify-center transition-colors text-base leading-6 border-none bg-transparent whitespace-nowrap ${
  74. active
  75. ? 'font-bold text-[#0FA4E9] border-b-2 border-[#0FA4E9]'
  76. : 'font-normal text-white/80 hover:text-white'
  77. }`}
  78. >
  79. {getMenuItemLabel(item)}
  80. </button>
  81. );
  82. })}
  83. {overflowMenuItems.length > 0 && (
  84. <Dropdown
  85. menu={overflowMenuProps}
  86. open={isOverflowMenuOpen}
  87. onOpenChange={setOverflowMenuOpen}
  88. overlayClassName="topbar-overflow-menu"
  89. trigger={['click']}
  90. placement="bottomRight"
  91. >
  92. <button
  93. type="button"
  94. className="px-2.5 h-10 flex items-center justify-center transition-colors text-base leading-6 border-none bg-transparent text-white/80 hover:text-white gap-1 whitespace-nowrap"
  95. >
  96. <span>...</span>
  97. <Icon
  98. icon={chevronDownIcon}
  99. className={`w-4 h-4 transition-transform ${
  100. isOverflowMenuOpen ? 'rotate-180' : ''
  101. }`}
  102. />
  103. </button>
  104. </Dropdown>
  105. )}
  106. </div>
  107. <div ref={languageButtonRef}>
  108. <LanguageSwitch className="px-2.5 h-10 flex items-center justify-center transition-colors text-base leading-6 border-none bg-transparent font-normal text-white/80 hover:text-white whitespace-nowrap cursor-pointer" />
  109. </div>
  110. {!isLoggedIn && (
  111. <button
  112. ref={loginButtonRef}
  113. type="button"
  114. onClick={() => openLoginDialog()}
  115. className="ms-2 px-4 h-10 rounded-full bg-[#0FA4E9] text-white text-base font-normal border-none hover:bg-[#0d93d1] transition-colors whitespace-nowrap"
  116. >
  117. {t('components.topbar.login')}
  118. </button>
  119. )}
  120. </nav>
  121. )}
  122. {/* Mobile Menu Button */}
  123. {isMobile && (
  124. <button
  125. type="button"
  126. onClick={toggleMobileMenu}
  127. className="p-2 rounded-xl bg-white/20 border-none text-white outline-none focus:outline-none [-webkit-tap-highlight-color:transparent] transition-colors"
  128. aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
  129. >
  130. <Icon
  131. icon={isMobileMenuOpen ? closeIcon : menuIcon}
  132. className="w-6 h-6"
  133. />
  134. </button>
  135. )}
  136. </div>
  137. </header>
  138. {/* Mobile Expanded Menu */}
  139. {isMobile && (isMobileMenuOpen || isMobileMenuClosing) && (
  140. <>
  141. <div
  142. className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40 top-[81px]"
  143. onClick={closeMobileMenu}
  144. />
  145. <nav
  146. className={`fixed end-5 top-[81px] z-50 border border-white/[0.35] bg-black/85 rounded-lg origin-top ${
  147. isMobileMenuClosing ? 'animate-collapse-up' : 'animate-expand-down'
  148. }`}
  149. onAnimationEnd={handleMenuAnimationEnd}
  150. >
  151. <div className="flex flex-col items-end gap-4 p-4">
  152. {menuItems.map((item: NavMenuItem) => {
  153. const active = isActive(item.path);
  154. return (
  155. <button
  156. key={item.name}
  157. onClick={() => handleMenuClick(item.path)}
  158. className={`text-base leading-[1.5] transition-colors border-none bg-transparent whitespace-nowrap ${
  159. active
  160. ? 'font-bold text-white'
  161. : 'font-normal text-[#999] hover:text-white'
  162. }`}
  163. >
  164. {getMenuItemLabel(item)}
  165. </button>
  166. );
  167. })}
  168. <LanguageSwitch className="text-base leading-[1.5] font-normal text-[#999] hover:text-white transition-colors border-none bg-transparent whitespace-nowrap cursor-pointer" />
  169. {!isLoggedIn && (
  170. <button
  171. type="button"
  172. onClick={() => {
  173. closeMobileMenu();
  174. openLoginDialog();
  175. }}
  176. className="text-base leading-[1.5] font-normal text-[#999] hover:text-white transition-colors border-none bg-transparent whitespace-nowrap"
  177. >
  178. {t('components.topbar.login')}
  179. </button>
  180. )}
  181. </div>
  182. </nav>
  183. </>
  184. )}
  185. </Fragment>
  186. );
  187. });
  188. Topbar.displayName = 'Topbar';
  189. export default Topbar;