Header.tsx 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. import React, { useState, useEffect, useRef } from "react";
  2. import { useNavigate, useLocation, Link } from "react-router-dom";
  3. import logo from "../assets/img/getgo.svg";
  4. import { useAppDispatch, useAppSelector } from "../hooks/useRedux";
  5. import { useMutation } from "@tanstack/react-query";
  6. import {
  7. startLoading,
  8. startSmallLoading,
  9. stopLoading,
  10. stopSmallLoading,
  11. } from "../features/loading/loadingSlice";
  12. import { productApi } from "../apis/productApi";
  13. import { Area } from "../services/product/type";
  14. import { accountLogout } from "../features/account/accuntSlice";
  15. import { useSelector } from "react-redux";
  16. import { setAreas } from "../features/areas/areasSlice";
  17. import i18n from "../i18n";
  18. import { useTranslation } from "react-i18next";
  19. import { getWithExpiry, setWithExpiry } from "../logic/loigicUtils";
  20. const Header: React.FC = () => {
  21. const navigate = useNavigate();
  22. // const areas = useSelector((state: any) => state.areas.areas);
  23. const areas = getWithExpiry<Area[] | []>("areas");
  24. const { t } = useTranslation();
  25. const location = useLocation();
  26. const [isMenuOpen, setIsMenuOpen] = useState(false);
  27. const [isBuySimExpanded, setIsBuySimExpanded] = useState(false);
  28. const [isGuideExpanded, setIsGuideExpanded] = useState(false);
  29. const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
  30. const [isBuySimMegaVisible, setIsBuySimMegaVisible] = useState(false);
  31. const [isGuideMegaVisible, setIsGuideMegaVisible] = useState(false);
  32. const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
  33. const lang = localStorage.getItem("lang") || "en";
  34. const [selectedLang, setSelectedLang] = useState<"en" | "vi">(lang);
  35. const [activeDesktopTab, setActiveDesktopTab] = useState<
  36. "popular" | "region"
  37. >("popular");
  38. const [isScrolled, setIsScrolled] = useState(false);
  39. const [products, setProducts] = useState<Area[]>([]);
  40. const dispatch = useAppDispatch();
  41. const langMenuRef = useRef<HTMLDivElement>(null);
  42. const userMenuRef = useRef<HTMLDivElement>(null);
  43. const account = localStorage.getItem("accountInfo");
  44. const guideItems = [
  45. { label: t("whatEsim"), path: "/support" },
  46. { label: t("installationInstructions"), path: "/support" },
  47. { label: t("supportSupport"), path: "/support" },
  48. { label: t("orderSearch"), path: "/support" },
  49. ];
  50. const dropdownRef = useRef<HTMLDivElement>(null);
  51. const [areasList, setAreasList] = useState<Area[]>([]);
  52. const [searchQuery, setSearchQuery] = useState("");
  53. const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
  54. const languages = [
  55. { code: "en", label: "English", flag: "us" },
  56. { code: "vi", label: "Tiếng Việt", flag: "vn" },
  57. ];
  58. // load product by country/region or popularity
  59. const getProductMutation = useMutation({
  60. mutationFn: async () => {
  61. dispatch(startLoading({}));
  62. const res = await productApi.loadArea(
  63. activeDesktopTab === "popular"
  64. ? { isCountry: "-1", isPopular: "1" }
  65. : { isCountry: "0", isPopular: "-1" }
  66. );
  67. return res;
  68. },
  69. onSuccess: (data) => {
  70. dispatch(stopLoading());
  71. if (data && data.errorCode === "0") {
  72. setProducts(data.data);
  73. } else {
  74. console.error("Get area failed, no token received");
  75. }
  76. },
  77. onError: (error: any) => {
  78. dispatch(stopLoading());
  79. console.error("Get area error:", error.response.data);
  80. },
  81. });
  82. useEffect(() => {
  83. const handleScroll = () => {
  84. setIsScrolled(window.scrollY > 300);
  85. };
  86. window.addEventListener("scroll", handleScroll);
  87. return () => window.removeEventListener("scroll", handleScroll);
  88. }, []);
  89. const handleAreaClick = (c: { id: number }) => {
  90. navigate(`/product/${c.id}`, {
  91. state: {
  92. ...c,
  93. },
  94. });
  95. setIsBuySimMegaVisible(false);
  96. setIsMenuOpen(false);
  97. };
  98. useEffect(() => {
  99. const handleClickOutside = (event: MouseEvent) => {
  100. if (
  101. langMenuRef.current &&
  102. !langMenuRef.current.contains(event.target as Node)
  103. ) {
  104. setIsLangMenuOpen(false);
  105. }
  106. };
  107. document.addEventListener("mousedown", handleClickOutside);
  108. return () => document.removeEventListener("mousedown", handleClickOutside);
  109. }, []);
  110. useEffect(() => {
  111. const handleResize = () => {
  112. if (window.innerWidth >= 1024) setIsMenuOpen(false);
  113. };
  114. window.addEventListener("resize", handleResize);
  115. return () => window.removeEventListener("resize", handleResize);
  116. }, []);
  117. const currentLangObj =
  118. languages.find((l) => l.code === selectedLang) || languages[0];
  119. const isActive = (path: string) => location.pathname === path;
  120. useEffect(() => {
  121. getProductMutation.mutate();
  122. }, [activeDesktopTab]);
  123. useEffect(() => {
  124. if (!areas || areas.length === 0) getAreaMutation.mutate();
  125. else {
  126. setAreasList(areas);
  127. console.log("Areas loaded from store:", areas);
  128. }
  129. }, []);
  130. const getAreaMutation = useMutation({
  131. mutationFn: async () => {
  132. dispatch(startLoading({}));
  133. const res = await productApi.loadArea({
  134. isCountry: "-1",
  135. isPopular: "-1",
  136. });
  137. return res;
  138. },
  139. onSuccess: (data) => {
  140. dispatch(stopLoading());
  141. console.log("Get area response data:", data);
  142. if (data && data.errorCode === "0") {
  143. console.log("Get area successful");
  144. setWithExpiry("areas", JSON.stringify(data.data));
  145. setAreasList(data.data as Area[]);
  146. } else {
  147. console.error("Get area failed, no token received");
  148. }
  149. },
  150. onError: (error: any) => {
  151. dispatch(stopLoading());
  152. console.error("Get area error:", error.response.data);
  153. },
  154. });
  155. const handleSelect = (area: Area) => {
  156. console.log("Selected area:", area);
  157. setIsSearchDropdownOpen(false);
  158. navigate(`/product/${area.id}`, {
  159. state: {
  160. ...area,
  161. },
  162. });
  163. };
  164. const handleSearch = (query: string) => {
  165. setSearchQuery(query);
  166. if (query.trim() === "") {
  167. setAreasList(areas);
  168. } else {
  169. const filtered = areas.filter((area: Area) =>
  170. area.coverageArea.toLowerCase().includes(query.toLowerCase())
  171. );
  172. setAreasList(filtered);
  173. }
  174. };
  175. useEffect(() => {
  176. const handleClickOutside = (event: MouseEvent) => {
  177. if (
  178. dropdownRef.current &&
  179. !dropdownRef.current.contains(event.target as Node)
  180. ) {
  181. setIsSearchDropdownOpen(false);
  182. }
  183. };
  184. document.addEventListener("mousedown", handleClickOutside);
  185. return () => {
  186. document.removeEventListener("mousedown", handleClickOutside);
  187. };
  188. }, []);
  189. return (
  190. <>
  191. <header className="sticky top-0 z-[60] w-full bg-white border-b border-slate-100 shadow-sm transition-all duration-300">
  192. <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
  193. <div className="flex justify-between items-center h-20">
  194. {/* Logo */}
  195. <Link to="/" className="flex-shrink-0 flex items-center">
  196. <div className="flex items-center space-x-1">
  197. {/* <svg className="w-8 h-8 text-[#EE0434]" viewBox="0 0 24 24" fill="currentColor">
  198. <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
  199. </svg> */}
  200. <img src={logo} alt="Getgo Logo" className="w-35" />
  201. {/* <span className="text-2xl font-bold tracking-tighter">
  202. <span className="text-[#EE0434]">Infi</span>
  203. <span className="text-[#333]">Gate</span>
  204. </span> */}
  205. </div>
  206. </Link>
  207. {/* Desktop Search on Scroll */}
  208. <div
  209. ref={dropdownRef}
  210. className={`hidden lg:flex items-center transition-all duration-500 ${
  211. isScrolled
  212. ? "flex-1 max-w-md mx-8 opacity-100 overflow-visible"
  213. : "max-w-0 opacity-0 pointer-events-none overflow-hidden"
  214. }`}
  215. >
  216. <div className="relative w-full">
  217. <input
  218. type="text"
  219. value={searchQuery}
  220. onChange={(e) => {
  221. handleSearch(e.target.value);
  222. }}
  223. onFocus={() => setIsSearchDropdownOpen(true)}
  224. placeholder={t("searchCountry")}
  225. className="w-full bg-slate-50 border border-slate-200 rounded-full py-2.5 px-6 pl-12 text-sm focus:outline-none focus:ring-2 focus:ring-red-100 focus:border-[#EE0434] transition-all"
  226. />
  227. <svg
  228. className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"
  229. fill="none"
  230. stroke="currentColor"
  231. viewBox="0 0 24 24"
  232. >
  233. <path
  234. strokeLinecap="round"
  235. strokeLinejoin="round"
  236. strokeWidth={2.5}
  237. d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
  238. />
  239. </svg>
  240. {/* Dropdown Menu */}
  241. {isSearchDropdownOpen && (
  242. <div className="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.1)] border border-slate-100 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
  243. <div className="max-h-[300px] overflow-y-auto custom-scrollbar p-2">
  244. <h3 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2 px-3 pt-2">
  245. {t("mostPopular")}
  246. </h3>
  247. <div className="space-y-0.5">
  248. {areasList.length > 0 ? (
  249. areasList.map((p) => (
  250. <button
  251. key={p.id}
  252. onClick={() => handleSelect(p)}
  253. className="w-full flex items-center justify-between p-2 hover:bg-slate-50 rounded-xl transition-all group"
  254. >
  255. <div className="flex items-center space-x-3">
  256. <div className="w-8 h-8 rounded-full border border-slate-100 overflow-hidden shadow-sm shrink-0">
  257. <img
  258. src={`${p.iconUrl}`}
  259. alt={p.areaName1}
  260. className="w-full h-full object-cover scale-150"
  261. />
  262. </div>
  263. <span className="font-bold text-slate-700 text-sm group-hover:text-[#EE0434] transition-colors">
  264. {p.areaName1}
  265. </span>
  266. </div>
  267. <div className="text-right flex items-center space-x-1.5">
  268. <span className="text-xs text-slate-400 font-medium">
  269. {t("from")}:
  270. </span>
  271. <span className="text-sm font-black text-[#EE0434]">
  272. {p.minSellPrice.toLocaleString()} {p.curency}
  273. </span>
  274. </div>
  275. </button>
  276. ))
  277. ) : (
  278. <div className="text-center py-4 text-slate-400 text-sm font-medium">
  279. {t("noMatchesFound")}
  280. </div>
  281. )}
  282. </div>
  283. </div>
  284. </div>
  285. )}
  286. </div>
  287. </div>
  288. {/* Desktop Nav */}
  289. <nav
  290. className={`hidden lg:flex items-center h-full transition-all duration-300 ${
  291. isScrolled ? "space-x-4" : "space-x-8"
  292. }`}
  293. >
  294. <Link
  295. to="/"
  296. className={`text-[17px] font-bold ${
  297. isActive("/")
  298. ? "text-[#EE0434]"
  299. : "text-slate-700 hover:text-[#EE0434]"
  300. }`}
  301. >
  302. {t("home")}
  303. </Link>
  304. <div
  305. className="relative h-full flex items-center"
  306. onMouseEnter={() => setIsBuySimMegaVisible(true)}
  307. onMouseLeave={() => setIsBuySimMegaVisible(false)}
  308. >
  309. <Link
  310. to="/buy-sim"
  311. className={`flex items-center text-[17px] font-bold transition-colors ${
  312. isActive("/buy-sim")
  313. ? "text-[#EE0434]"
  314. : "text-slate-700 hover:text-[#EE0434]"
  315. }`}
  316. >
  317. {t("buySim")}{" "}
  318. <svg
  319. className={`ml-1 w-4 h-4 transition-transform ${
  320. isBuySimMegaVisible ? "rotate-180" : ""
  321. }`}
  322. fill="none"
  323. stroke="currentColor"
  324. viewBox="0 0 24 24"
  325. >
  326. <path
  327. strokeLinecap="round"
  328. strokeLinejoin="round"
  329. strokeWidth={2}
  330. d="M19 9l-7 7-7-7"
  331. />
  332. </svg>
  333. </Link>
  334. {isBuySimMegaVisible && (
  335. <div className="absolute top-full left-1/2 -translate-x-1/2 w-[950px] bg-white rounded-[32px] shadow-[0_30px_60px_-15px_rgba(0,0,0,0.15)] border border-slate-100 mt-0 overflow-hidden flex animate-in fade-in slide-in-from-top-2 duration-300">
  336. <div className="w-[280px] bg-red-50 p-10 flex flex-col">
  337. <h3 className="text-4xl font-black text-slate-900 mb-4">
  338. {t("buySim")}
  339. </h3>
  340. <button
  341. onClick={() => {
  342. navigate("/buy-sim");
  343. setIsBuySimMegaVisible(false);
  344. }}
  345. className="text-[#EE0434] font-bold text-xl flex items-center group mb-8"
  346. >
  347. {t("viewAll")}{" "}
  348. <svg
  349. className="ml-2 w-5 h-5 transition-transform group-hover:translate-x-1"
  350. fill="none"
  351. stroke="currentColor"
  352. viewBox="0 0 24 24"
  353. >
  354. <path
  355. strokeLinecap="round"
  356. strokeLinejoin="round"
  357. strokeWidth={3}
  358. d="M9 5l7 7-7 7"
  359. />
  360. </svg>
  361. </button>
  362. </div>
  363. <div className="flex-1 p-10">
  364. <div className="flex space-x-4 mb-10">
  365. <button
  366. onClick={() => setActiveDesktopTab("popular")}
  367. className={`px-8 py-3 rounded-full text-base font-bold transition-all ${
  368. activeDesktopTab === "popular"
  369. ? "bg-[#EE0434] text-white shadow-lg shadow-red-100"
  370. : "bg-slate-50 text-slate-900 hover:bg-slate-100"
  371. }`}
  372. >
  373. {t("mostPopular")}
  374. </button>
  375. <button
  376. onClick={() => setActiveDesktopTab("region")}
  377. className={`px-8 py-3 rounded-full text-base font-bold transition-all ${
  378. activeDesktopTab === "region"
  379. ? "bg-[#EE0434] text-white shadow-lg shadow-red-100"
  380. : "bg-slate-50 text-slate-900 hover:bg-slate-100"
  381. }`}
  382. >
  383. {t("region")}
  384. </button>
  385. </div>
  386. <div className="grid grid-cols-4 gap-y-8 gap-x-4">
  387. {products.map((p) => (
  388. <div
  389. key={p.id}
  390. onClick={() => handleAreaClick(p)}
  391. className="flex items-center space-x-3 group cursor-pointer hover:bg-slate-50 p-2 rounded-xl transition-colors"
  392. >
  393. <div className="w-7 h-7 rounded-full overflow-hidden border border-slate-200 shadow-sm shrink-0">
  394. <img
  395. src={`${p.iconUrl}`}
  396. alt={p.areaName1}
  397. className="w-full h-full object-cover"
  398. />
  399. </div>
  400. <span className="text-[16px] font-bold text-slate-700 group-hover:text-[#EE0434] transition-colors">
  401. {p.areaName1}
  402. </span>
  403. </div>
  404. ))}
  405. </div>
  406. </div>
  407. </div>
  408. )}
  409. </div>
  410. <div
  411. className="relative h-full flex items-center"
  412. onMouseEnter={() => setIsGuideMegaVisible(true)}
  413. onMouseLeave={() => setIsGuideMegaVisible(false)}
  414. >
  415. <Link
  416. to="/support"
  417. className={`flex items-center text-[17px] font-bold transition-colors ${
  418. isActive("/support")
  419. ? "text-[#EE0434]"
  420. : "text-slate-700 hover:text-[#EE0434]"
  421. }`}
  422. >
  423. {t("guide")}{" "}
  424. <svg
  425. className={`ml-1 w-4 h-4 transition-transform ${
  426. isGuideMegaVisible ? "rotate-180" : ""
  427. }`}
  428. fill="none"
  429. stroke="currentColor"
  430. viewBox="0 0 24 24"
  431. >
  432. <path
  433. strokeLinecap="round"
  434. strokeLinejoin="round"
  435. strokeWidth={2}
  436. d="M19 9l-7 7-7-7"
  437. />
  438. </svg>
  439. </Link>
  440. {isGuideMegaVisible && (
  441. <div className="absolute top-full left-0 w-[600px] bg-white rounded-[32px] shadow-2xl border border-slate-100 mt-0 overflow-hidden flex animate-in fade-in slide-in-from-top-2 duration-300">
  442. <div className="flex-1 py-10 px-6 flex flex-col">
  443. {guideItems.map((item, idx) => (
  444. <button
  445. key={item.label}
  446. onClick={() => {
  447. navigate(item.path);
  448. setIsGuideMegaVisible(false);
  449. }}
  450. className={`w-full text-left px-6 py-4 rounded-2xl text-base font-medium transition-all text-slate-600 hover:bg-slate-50 hover:text-[#EE0434]`}
  451. >
  452. {item.label}
  453. </button>
  454. ))}
  455. </div>
  456. </div>
  457. )}
  458. </div>
  459. <Link
  460. to="/news"
  461. className={`text-[17px] font-bold transition-colors ${
  462. isActive("/news")
  463. ? "text-[#EE0434]"
  464. : "text-slate-700 hover:text-[#EE0434]"
  465. }`}
  466. >
  467. {t("news")}
  468. </Link>
  469. <Link
  470. to="/contact"
  471. className={`text-[17px] font-bold transition-colors ${
  472. isActive("/contact")
  473. ? "text-[#EE0434]"
  474. : "text-slate-700 hover:text-[#EE0434]"
  475. }`}
  476. >
  477. {t("contact")}
  478. </Link>
  479. </nav>
  480. {/* Icons */}
  481. <div className="flex items-center space-x-5">
  482. {/* Account Dropdown */}
  483. <div className="relative hidden lg:block" ref={userMenuRef}>
  484. <button
  485. onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
  486. className="p-2 text-slate-700 hover:text-[#EE0434] transition-colors rounded-full hover:bg-slate-50"
  487. title="Account"
  488. >
  489. <svg
  490. className="w-6 h-6"
  491. fill="none"
  492. stroke="currentColor"
  493. viewBox="0 0 24 24"
  494. >
  495. <path
  496. strokeLinecap="round"
  497. strokeLinejoin="round"
  498. strokeWidth={2}
  499. d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
  500. />
  501. </svg>
  502. </button>
  503. {isUserMenuOpen && (
  504. <div className="absolute top-full right-0 mt-3 w-56 bg-white rounded-[24px] shadow-[0_20px_40px_rgba(0,0,0,0.1)] border border-slate-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200 z-50">
  505. {account !== null && (
  506. <div className="flex flex-col py-2">
  507. <button
  508. onClick={() => {
  509. navigate("/order-history");
  510. setIsUserMenuOpen(false);
  511. }}
  512. className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
  513. >
  514. <svg
  515. className="w-5 h-5 text-slate-400"
  516. fill="none"
  517. stroke="currentColor"
  518. viewBox="0 0 24 24"
  519. >
  520. <path
  521. strokeLinecap="round"
  522. strokeLinejoin="round"
  523. strokeWidth={2}
  524. d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
  525. />
  526. </svg>
  527. <span className="font-bold text-sm">
  528. {t("orders")}
  529. </span>
  530. </button>
  531. <div className="h-px bg-slate-50 mx-4 my-1"></div>
  532. <button
  533. onClick={() => {
  534. setIsUserMenuOpen(false);
  535. dispatch(accountLogout());
  536. navigate("/login");
  537. }}
  538. className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
  539. >
  540. <svg
  541. className="w-5 h-5 text-slate-400"
  542. fill="none"
  543. stroke="currentColor"
  544. viewBox="0 0 24 24"
  545. >
  546. <path
  547. strokeLinecap="round"
  548. strokeLinejoin="round"
  549. strokeWidth={2}
  550. d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
  551. />
  552. </svg>
  553. <span className="font-bold text-sm">
  554. {t("logout")}
  555. </span>
  556. </button>
  557. </div>
  558. )}
  559. {account === null && (
  560. <div className="flex flex-col py-2">
  561. <button
  562. onClick={() => {
  563. setIsUserMenuOpen(false);
  564. dispatch(accountLogout());
  565. navigate("/login");
  566. }}
  567. className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
  568. >
  569. <svg
  570. className="w-5 h-5 text-slate-400"
  571. fill="none"
  572. stroke="currentColor"
  573. viewBox="0 0 24 24"
  574. >
  575. <path
  576. strokeLinecap="round"
  577. strokeLinejoin="round"
  578. strokeWidth={2}
  579. d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
  580. />
  581. </svg>
  582. <span className="font-bold text-sm">
  583. {t("login")}
  584. </span>
  585. </button>
  586. </div>
  587. )}
  588. </div>
  589. )}
  590. </div>
  591. {/* <button className="hidden sm:flex p-2 text-slate-700 hover:text-[#EE0434] relative">
  592. <svg
  593. className="w-6 h-6"
  594. fill="none"
  595. stroke="currentColor"
  596. viewBox="0 0 24 24"
  597. >
  598. <path
  599. strokeLinecap="round"
  600. strokeLinejoin="round"
  601. strokeWidth={2}
  602. d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
  603. />
  604. </svg>
  605. <span className="absolute top-1 right-1 w-4 h-4 bg-[#EE0434] text-white text-[10px] flex items-center justify-center rounded-full font-black">
  606. 0
  607. </span>
  608. </button> */}
  609. <div className="relative" ref={langMenuRef}>
  610. <button
  611. onClick={() => setIsLangMenuOpen(!isLangMenuOpen)}
  612. className="bg-white p-1 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all flex items-center justify-center"
  613. >
  614. <div className="w-8 h-8 rounded-full overflow-hidden border border-slate-50">
  615. <img
  616. src={`https://flagcdn.com/w80/${currentLangObj.flag}.png`}
  617. alt={currentLangObj.label}
  618. className="w-full h-full object-cover"
  619. />
  620. </div>
  621. </button>
  622. {isLangMenuOpen && (
  623. <div className="absolute top-full right-0 mt-3 w-48 bg-white rounded-[24px] shadow-[0_20px_40px_rgba(0,0,0,0.1)] border border-slate-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
  624. <div className="flex flex-col">
  625. {languages.map((lang) => (
  626. <button
  627. key={lang.code}
  628. onClick={() => {
  629. setSelectedLang(lang.code as "en" | "vi");
  630. setIsLangMenuOpen(false);
  631. i18n.changeLanguage(lang.code);
  632. localStorage.setItem("lang", lang.code);
  633. }}
  634. className={`flex items-center space-x-3 px-5 py-4 w-full text-left transition-colors ${
  635. selectedLang === lang.code
  636. ? "bg-slate-50"
  637. : "hover:bg-slate-50/50"
  638. }`}
  639. >
  640. <div className="w-7 h-7 rounded-full overflow-hidden border border-slate-100 shadow-sm">
  641. <img
  642. src={`https://flagcdn.com/w80/${lang.flag}.png`}
  643. alt={lang.label}
  644. className="w-full h-full object-cover"
  645. />
  646. </div>
  647. <span
  648. className={`text-[15px] font-bold ${
  649. selectedLang === lang.code
  650. ? "text-slate-900"
  651. : "text-slate-500"
  652. }`}
  653. >
  654. {lang.label}
  655. </span>
  656. </button>
  657. ))}
  658. </div>
  659. </div>
  660. )}
  661. </div>
  662. <button
  663. onClick={() => setIsMenuOpen(true)}
  664. className="lg:hidden p-2 text-slate-900"
  665. >
  666. <svg
  667. className="w-6 h-6"
  668. fill="none"
  669. stroke="currentColor"
  670. viewBox="0 0 24 24"
  671. >
  672. <path
  673. strokeLinecap="round"
  674. strokeLinejoin="round"
  675. strokeWidth={2.5}
  676. d="M4 6h16M4 12h16m-7 6h7"
  677. />
  678. </svg>
  679. </button>
  680. </div>
  681. </div>
  682. </div>
  683. </header>
  684. {/* Full-Screen Mobile Menu with Slide-Right Transition */}
  685. <div
  686. className={`fixed inset-0 z-[100] lg:hidden transition-all duration-500 ease-in-out ${
  687. isMenuOpen
  688. ? "opacity-100 pointer-events-auto"
  689. : "opacity-0 pointer-events-none"
  690. }`}
  691. >
  692. <div
  693. className={`absolute inset-0 bg-white flex flex-col transform transition-transform duration-500 ease-out ${
  694. isMenuOpen ? "translate-x-0" : "-translate-x-full"
  695. }`}
  696. >
  697. {/* Mobile Menu Header */}
  698. <div className="flex justify-between items-center p-6 border-b border-slate-50">
  699. <Link
  700. to="/"
  701. onClick={() => setIsMenuOpen(false)}
  702. className="flex items-center space-x-1"
  703. >
  704. <svg
  705. className="w-7 h-7 text-[#EE0434]"
  706. viewBox="0 0 24 24"
  707. fill="currentColor"
  708. >
  709. <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
  710. </svg>
  711. <span className="text-xl font-black tracking-tighter">
  712. <span className="text-[#EE0434]">Get</span>
  713. <span className="text-[#333]">Go</span>
  714. </span>
  715. </Link>
  716. <button
  717. onClick={() => setIsMenuOpen(false)}
  718. className="p-2 text-slate-400 hover:text-[#EE0434] transition-colors rounded-full hover:bg-slate-100"
  719. >
  720. <svg
  721. className="w-8 h-8"
  722. fill="none"
  723. stroke="currentColor"
  724. viewBox="0 0 24 24"
  725. >
  726. <path
  727. strokeLinecap="round"
  728. strokeLinejoin="round"
  729. strokeWidth={2}
  730. d="M6 18L18 6M6 6l12 12"
  731. />
  732. </svg>
  733. </button>
  734. </div>
  735. <div className="flex-1 overflow-y-auto px-6 py-8 space-y-6">
  736. <div className="flex flex-col items-center space-y-4">
  737. <Link
  738. to="/"
  739. onClick={() => setIsMenuOpen(false)}
  740. className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
  741. isActive("/")
  742. ? "bg-red-50 text-[#EE0434]"
  743. : "text-slate-800 hover:bg-slate-50"
  744. }`}
  745. >
  746. {t("home")}
  747. </Link>
  748. <div className="w-full">
  749. <button
  750. onClick={() => setIsBuySimExpanded(!isBuySimExpanded)}
  751. className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
  752. isActive("/buy-sim")
  753. ? "bg-red-50 text-[#EE0434]"
  754. : "text-slate-800 hover:bg-slate-50"
  755. }`}
  756. >
  757. <span>{t("buySim")}</span>
  758. <svg
  759. className={`w-6 h-6 transition-transform duration-300 ${
  760. isBuySimExpanded ? "rotate-180" : ""
  761. }`}
  762. fill="none"
  763. stroke="currentColor"
  764. viewBox="0 0 24 24"
  765. >
  766. <path
  767. strokeLinecap="round"
  768. strokeLinejoin="round"
  769. strokeWidth={2.5}
  770. d="M19 9l-7 7-7-7"
  771. />
  772. </svg>
  773. </button>
  774. <div
  775. className={`overflow-hidden transition-all duration-300 ease-in-out ${
  776. isBuySimExpanded
  777. ? "max-h-[1000px] opacity-100 mt-4"
  778. : "max-h-0 opacity-0"
  779. }`}
  780. >
  781. <div className="grid grid-cols-2 gap-3 px-2">
  782. <button
  783. onClick={() => {
  784. navigate("/buy-sim");
  785. setIsMenuOpen(false);
  786. }}
  787. className="col-span-2 text-center py-4 bg-slate-50 rounded-2xl text-[#EE0434] font-black text-sm uppercase tracking-wider shadow-sm"
  788. >
  789. {t("viewAllDestinations")} →
  790. </button>
  791. {products.map((c) => (
  792. <button
  793. key={c.areaName1}
  794. onClick={() => handleAreaClick(c)}
  795. className="flex flex-col items-center justify-center space-y-2 py-5 rounded-2xl bg-white border border-slate-100 shadow-sm active:bg-slate-50"
  796. >
  797. <img
  798. src={`${c.iconUrl}`}
  799. alt={c.areaName1}
  800. className="w-10 h-10 rounded-full object-cover border-2 border-slate-50"
  801. />
  802. <span className="text-sm font-bold text-slate-700">
  803. {c.areaName1}
  804. </span>
  805. </button>
  806. ))}
  807. </div>
  808. </div>
  809. </div>
  810. <div className="w-full">
  811. <button
  812. onClick={() => setIsGuideExpanded(!isGuideExpanded)}
  813. className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
  814. isActive("/support")
  815. ? "bg-red-50 text-[#EE0434]"
  816. : "text-slate-800 hover:bg-slate-50"
  817. }`}
  818. >
  819. <span>{t("guide")}</span>
  820. <svg
  821. className={`w-6 h-6 transition-transform duration-300 ${
  822. isGuideExpanded ? "rotate-180" : ""
  823. }`}
  824. fill="none"
  825. stroke="currentColor"
  826. viewBox="0 0 24 24"
  827. >
  828. <path
  829. strokeLinecap="round"
  830. strokeLinejoin="round"
  831. strokeWidth={2.5}
  832. d="M19 9l-7 7-7-7"
  833. />
  834. </svg>
  835. </button>
  836. <div
  837. className={`overflow-hidden transition-all duration-300 ease-in-out ${
  838. isGuideExpanded
  839. ? "max-h-[400px] opacity-100 mt-4"
  840. : "max-h-0 opacity-0"
  841. }`}
  842. >
  843. <div className="flex flex-col space-y-2 px-2">
  844. {guideItems.map((item) => (
  845. <button
  846. key={item.label}
  847. onClick={() => {
  848. navigate(item.path);
  849. setIsMenuOpen(false);
  850. }}
  851. className="w-full text-center py-4 rounded-2xl bg-slate-50 text-slate-600 font-bold hover:text-[#EE0434] active:bg-red-50"
  852. >
  853. {item.label}
  854. </button>
  855. ))}
  856. </div>
  857. </div>
  858. </div>
  859. <Link
  860. to="/news"
  861. onClick={() => setIsMenuOpen(false)}
  862. className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
  863. isActive("/news")
  864. ? "bg-red-50 text-[#EE0434]"
  865. : "text-slate-800 hover:bg-slate-50"
  866. }`}
  867. >
  868. {t("news")}
  869. </Link>
  870. <Link
  871. to="/contact"
  872. onClick={() => setIsMenuOpen(false)}
  873. className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
  874. isActive("/contact")
  875. ? "bg-red-50 text-[#EE0434]"
  876. : "text-slate-800 hover:bg-slate-50"
  877. }`}
  878. >
  879. {t("contact")}
  880. </Link>
  881. {account !== null && (
  882. <button
  883. onClick={() => {
  884. navigate("/order-history");
  885. setIsMenuOpen(false);
  886. }}
  887. className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
  888. isActive("/order-history")
  889. ? "bg-red-50 text-[#EE0434]"
  890. : "text-slate-800 hover:bg-slate-50"
  891. }`}
  892. >
  893. {t("transactionHistory")}
  894. </button>
  895. )}
  896. <div className="w-full pt-4">
  897. <p className="text-center text-slate-400 font-bold text-xs uppercase tracking-widest mb-4">
  898. {t("selectLanguage")}
  899. </p>
  900. <div className="flex justify-center space-x-4">
  901. {languages.map((lang) => (
  902. <button
  903. key={lang.code}
  904. onClick={() => {
  905. setSelectedLang(lang.code as "en" | "vi");
  906. setIsLangMenuOpen(false);
  907. i18n.changeLanguage(lang.code);
  908. localStorage.setItem("lang", lang.code);
  909. }}
  910. className={`flex items-center space-x-2 px-4 py-2 rounded-2xl transition-all border ${
  911. selectedLang === lang.code
  912. ? "bg-red-50 border-[#EE0434] text-[#EE0434]"
  913. : "bg-white border-slate-100 text-slate-500"
  914. }`}
  915. >
  916. <img
  917. src={`https://flagcdn.com/w40/${lang.flag}.png`}
  918. alt={lang.label}
  919. className="w-6 h-6 rounded-full object-cover border border-slate-100"
  920. />
  921. <span className="font-bold">{lang.label}</span>
  922. </button>
  923. ))}
  924. </div>
  925. </div>
  926. </div>
  927. </div>
  928. <div className="p-8 border-t border-slate-50 bg-slate-50/50">
  929. {account === null && (
  930. <Link
  931. to="/login"
  932. onClick={() => {
  933. setIsMenuOpen(false);
  934. dispatch(accountLogout());
  935. }}
  936. className="w-full bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white py-5 rounded-[40px] font-black text-2xl shadow-xl active:scale-[0.98] transition-all flex justify-center"
  937. >
  938. {t("login")} / {t("register")}
  939. </Link>
  940. )}
  941. {account !== null && (
  942. <Link
  943. to="/login"
  944. onClick={() => {
  945. setIsMenuOpen(false);
  946. dispatch(accountLogout());
  947. }}
  948. className="w-full bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white py-5 rounded-[40px] font-black text-2xl shadow-xl active:scale-[0.98] transition-all flex justify-center"
  949. >
  950. {t("logout")}
  951. </Link>
  952. )}
  953. </div>
  954. </div>
  955. </div>
  956. </>
  957. );
  958. };
  959. export default Header;