Quellcode durchsuchen

Add Tailwind CSS and new API modules

Integrated Tailwind CSS with Vite by adding dependencies and index.css. Introduced new API modules for article, auth, and content, along with related type definitions. Updated various components and pages to utilize new APIs and styles.
trunghieubui vor 4 Wochen
Ursprung
Commit
f356e903e2
28 geänderte Dateien mit 2112 neuen und 622 gelöschten Zeilen
  1. 52 46
      EsimLao/esim-vite/index.html
  2. 547 59
      EsimLao/esim-vite/package-lock.json
  3. 3 1
      EsimLao/esim-vite/package.json
  4. 36 0
      EsimLao/esim-vite/src/apis/articleApi.ts
  5. 19 0
      EsimLao/esim-vite/src/apis/authApi.ts
  6. 13 7
      EsimLao/esim-vite/src/apis/baseApi.ts
  7. 57 0
      EsimLao/esim-vite/src/apis/contentApi.ts
  8. 9 10
      EsimLao/esim-vite/src/apis/productApi.ts
  9. 65 0
      EsimLao/esim-vite/src/assets/img/getgo.svg
  10. 446 145
      EsimLao/esim-vite/src/components/Header.tsx
  11. 10 7
      EsimLao/esim-vite/src/components/TestimonialCard.tsx
  12. 15 11
      EsimLao/esim-vite/src/components/TopLoader.tsx
  13. 32 0
      EsimLao/esim-vite/src/features/account/accuntSlice.ts
  14. 32 18
      EsimLao/esim-vite/src/features/loading/loadingSlice.ts
  15. 16 0
      EsimLao/esim-vite/src/global/constants.ts
  16. 1 0
      EsimLao/esim-vite/src/index.css
  17. 43 35
      EsimLao/esim-vite/src/pages/home/components/HomeBanner.tsx
  18. 49 20
      EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx
  19. 34 102
      EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx
  20. 88 54
      EsimLao/esim-vite/src/pages/home/components/HomeTestimonial.tsx
  21. 320 22
      EsimLao/esim-vite/src/pages/login/LoginView.tsx
  22. 46 11
      EsimLao/esim-vite/src/pages/news/NewsDetailView.tsx
  23. 36 54
      EsimLao/esim-vite/src/pages/news/NewsView.tsx
  24. 49 0
      EsimLao/esim-vite/src/services/article/types.ts
  25. 9 0
      EsimLao/esim-vite/src/services/auth/types.ts
  26. 62 0
      EsimLao/esim-vite/src/services/content/types.ts
  27. 14 12
      EsimLao/esim-vite/src/services/product/type.ts
  28. 9 8
      EsimLao/esim-vite/vite.config.ts

+ 52 - 46
EsimLao/esim-vite/index.html

@@ -1,44 +1,48 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>InfiGate | Stay Connected Everywhere</title>
-    <script src="https://cdn.tailwindcss.com"></script>
-    <link rel="preconnect" href="https://fonts.googleapis.com" />
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
-    <link
-      href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap"
-      rel="stylesheet"
-    />
-    <style>
-      body {
-        font-family: "Atkinson Hyperlegible", sans-serif;
-        background-color: #ffffff;
-        color: #1e293b;
-        margin: 0;
-        padding: 0;
-      }
-      /* Custom Scrollbar - Cleaner for Light UI */
-      ::-webkit-scrollbar {
-        width: 8px;
-      }
-      ::-webkit-scrollbar-track {
-        background: #f8fafc;
-      }
-      ::-webkit-scrollbar-thumb {
-        background: #cbd5e1;
-        border-radius: 10px;
-      }
-      ::-webkit-scrollbar-thumb:hover {
-        background: #94a3b8;
-      }
-      .infigate-shadow {
-        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05),
-          0 2px 4px -1px rgba(0, 0, 0, 0.03);
-      }
-    </style>
-    <script type="importmap">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>InfiGate | Stay Connected Everywhere</title>
+  <link rel="preconnect" href="https://fonts.googleapis.com" />
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+  <link
+    href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap"
+    rel="stylesheet" />
+  <style>
+    body {
+      font-family: "Atkinson Hyperlegible", sans-serif;
+      background-color: #ffffff;
+      color: #1e293b;
+      margin: 0;
+      padding: 0;
+    }
+
+    /* Custom Scrollbar - Cleaner for Light UI */
+    ::-webkit-scrollbar {
+      width: 8px;
+    }
+
+    ::-webkit-scrollbar-track {
+      background: #f8fafc;
+    }
+
+    ::-webkit-scrollbar-thumb {
+      background: #cbd5e1;
+      border-radius: 10px;
+    }
+
+    ::-webkit-scrollbar-thumb:hover {
+      background: #94a3b8;
+    }
+
+    .infigate-shadow {
+      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05),
+        0 2px 4px -1px rgba(0, 0, 0, 0.03);
+    }
+  </style>
+  <script type="importmap">
       {
         "imports": {
           "react/": "https://esm.sh/react@^19.2.3/",
@@ -49,10 +53,12 @@
         }
       }
     </script>
-    <link rel="stylesheet" href="./src/index.css" />
-  </head>
-  <body>
-    <div id="root"></div>
-    <script type="module" src="./src/index.tsx"></script>
-  </body>
-</html>
+  <link rel="stylesheet" href="./src/index.css" />
+</head>
+
+<body>
+  <div id="root"></div>
+  <script type="module" src="./src/index.tsx"></script>
+</body>
+
+</html>

Datei-Diff unterdrückt, da er zu groß ist
+ 547 - 59
EsimLao/esim-vite/package-lock.json


+ 3 - 1
EsimLao/esim-vite/package.json

@@ -11,6 +11,7 @@
   "dependencies": {
     "@google/genai": "^1.34.0",
     "@reduxjs/toolkit": "^2.11.2",
+    "@tailwindcss/vite": "^4.1.18",
     "@tanstack/react-query": "^5.90.16",
     "axios": "^1.13.2",
     "meta": "^2.2.25",
@@ -18,7 +19,8 @@
     "react-dom": "^19.2.3",
     "react-redux": "^9.2.0",
     "react-router-dom": "^7.11.0",
-    "redux": "^5.0.1"
+    "redux": "^5.0.1",
+    "tailwindcss": "^4.1.18"
   },
   "devDependencies": {
     "@types/node": "^22.14.0",

+ 36 - 0
EsimLao/esim-vite/src/apis/articleApi.ts

@@ -0,0 +1,36 @@
+import {
+  LoadArticleResponse,
+  LoadCategoryResponse,
+} from "../services/article/types";
+import { BaseApi } from "./baseApi";
+
+class ArticleApi extends BaseApi {
+  constructor() {
+    super("/article");
+  }
+
+  async LoadCategory({ pageNumber, pageSize, parentId }) {
+    return this.authPost<LoadCategoryResponse>("/category", {
+      pageNumber,
+      pageSize,
+      parentId,
+    });
+  }
+
+  async LoadArticle({ pageNumber, pageSize, categoryId, isFeatured }) {
+    return this.authPost<LoadArticleResponse>("/load", {
+      pageNumber,
+      pageSize,
+      categoryId,
+      isFeatured,
+    });
+  }
+
+  async LoadArticleDetail({ articleId }) {
+    return this.authPost<LoadArticleResponse>("/load-detail", {
+      articleId,
+    });
+  }
+}
+
+export const articleApi = new ArticleApi();

+ 19 - 0
EsimLao/esim-vite/src/apis/authApi.ts

@@ -0,0 +1,19 @@
+import { AccountInfo } from "../services/auth/types";
+import { AreaData } from "../services/product/type";
+import { BaseApi } from "./baseApi";
+
+class AuthApi extends BaseApi {
+  constructor() {
+    super("/auth");
+  }
+
+  async requestOtp({ email }) {
+    return this.authPost("/request-otp", { email });
+  }
+
+  async verifyOtp({ email, otp }) {
+    return this.authPost<AccountInfo>("/verify-otp", { email, otpCode: otp });
+  }
+}
+
+export const authApi = new AuthApi();

+ 13 - 7
EsimLao/esim-vite/src/apis/baseApi.ts

@@ -1,14 +1,13 @@
 import { createAxiosInstance } from "./axios";
 
 export type Response<T> = {
-  errorCode: number;
+  errorCode: string;
   message: string;
   msg: string;
   status: boolean;
   data: T;
 };
 
-
 export const authApiInstance = createAxiosInstance(
   import.meta.env.VITE_API_AUTH_URL
 );
@@ -29,17 +28,24 @@ export class BaseApi {
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  async productPost<T>(endpoint: string = '', data?: any, timeout?: number): Promise<Response<T>> {
+  async productPost<T>(
+    endpoint: string = "",
+    data?: any,
+    timeout?: number
+  ): Promise<Response<T>> {
     return productApiInstance.post(`${this.baseUrl}${endpoint}`, data, {
-      timeout: timeout ?? this.defaultTimeout
+      timeout: timeout ?? this.defaultTimeout,
     });
   }
 
-
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  async authPost<T>(endpoint: string = '', data?: any, timeout?: number): Promise<Response<T>> {
+  async authPost<T>(
+    endpoint: string = "",
+    data?: any,
+    timeout?: number
+  ): Promise<Response<T>> {
     return authApiInstance.post(`${this.baseUrl}${endpoint}`, data, {
-      timeout: timeout ?? this.defaultTimeout
+      timeout: timeout ?? this.defaultTimeout,
     });
   }
 }

+ 57 - 0
EsimLao/esim-vite/src/apis/contentApi.ts

@@ -0,0 +1,57 @@
+import {
+  LoadArticleResponse,
+  LoadCategoryResponse,
+} from "../services/article/types";
+import {
+  LoadBannerResponse,
+  LoadFaqCategoryResponse,
+  LoadFaqResponse,
+  LoadReviewResponse,
+} from "../services/content/types";
+import { BaseApi } from "./baseApi";
+
+class ContentApi extends BaseApi {
+  constructor() {
+    super("/content");
+  }
+
+  async LoadBanner({ pageNumber, pageSize, position = "home" }) {
+    return this.authPost<LoadBannerResponse>("/banner", {
+      pageNumber,
+      pageSize,
+      position,
+    });
+  }
+
+  async LoadReview({ pageNumber, pageSize, isFeatured = true }) {
+    return this.authPost<LoadReviewResponse>("/review", {
+      pageNumber,
+      pageSize,
+      isFeatured,
+    });
+  }
+
+  async LoadFaqCategory({ pageNumber, pageSize, parentId = null }) {
+    return this.authPost<LoadFaqCategoryResponse>("/faq-category", {
+      pageNumber,
+      pageSize,
+      parentId,
+    });
+  }
+
+  async LoadFaq({
+    pageNumber,
+    pageSize,
+    categoryId = null,
+    isFeatured = true,
+  }) {
+    return this.authPost<LoadFaqResponse>("/faq", {
+      pageNumber,
+      pageSize,
+      categoryId,
+      isFeatured,
+    });
+  }
+}
+
+export const contentApi = new ContentApi();

+ 9 - 10
EsimLao/esim-vite/src/apis/productApi.ts

@@ -1,15 +1,14 @@
-import { AreaData } from '../services/product/type';
-import { BaseApi } from './baseApi';
+import { AreaData } from "../services/product/type";
+import { BaseApi } from "./baseApi";
 
 class ProductApi extends BaseApi {
-    constructor() {
-        super('/api');
-    }
+  constructor() {
+    super("/api");
+  }
 
-    async loadArea() {
-        return this.productPost('/list_area');
-    }
+  async loadArea({ isCountry = "-1", isPopular = "-1" }) {
+    return this.productPost("/list_area", { isCountry, isPopular });
+  }
 }
 
-
-export const productApi = new ProductApi();
+export const productApi = new ProductApi();

Datei-Diff unterdrückt, da er zu groß ist
+ 65 - 0
EsimLao/esim-vite/src/assets/img/getgo.svg


+ 446 - 145
EsimLao/esim-vite/src/components/Header.tsx

@@ -1,6 +1,6 @@
-
-import React, { useState, useEffect, useRef } from 'react';
-import { useNavigate, useLocation, Link } from 'react-router-dom';
+import React, { useState, useEffect, useRef } from "react";
+import { useNavigate, useLocation, Link } from "react-router-dom";
+import logo from "../assets/img/getgo.svg";
 
 const Header: React.FC = () => {
   const navigate = useNavigate();
@@ -11,81 +11,103 @@ const Header: React.FC = () => {
   const [isBuySimMegaVisible, setIsBuySimMegaVisible] = useState(false);
   const [isGuideMegaVisible, setIsGuideMegaVisible] = useState(false);
   const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
-  const [selectedLang, setSelectedLang] = useState<'en' | 'vi'>('en');
-  const [activeDesktopTab, setActiveDesktopTab] = useState<'popular' | 'region'>('popular');
+  const [selectedLang, setSelectedLang] = useState<"en" | "vi">("en");
+  const [activeDesktopTab, setActiveDesktopTab] = useState<
+    "popular" | "region"
+  >("popular");
   const [isScrolled, setIsScrolled] = useState(false);
-  
+
   const langMenuRef = useRef<HTMLDivElement>(null);
 
   const countries = [
-    { name: 'China', flag: 'cn' },
-    { name: 'Hong Kong', flag: 'hk' },
-    { name: 'Japan', flag: 'jp' },
-    { name: 'Singapore', flag: 'sg' },
-    { name: 'South Korea', flag: 'kr' },
-    { name: 'Taiwan', flag: 'tw' },
-    { name: 'Thailand', flag: 'th' },
-    { name: 'United States', flag: 'us' },
+    { name: "China", flag: "cn" },
+    { name: "Hong Kong", flag: "hk" },
+    { name: "Japan", flag: "jp" },
+    { name: "Singapore", flag: "sg" },
+    { name: "South Korea", flag: "kr" },
+    { name: "Taiwan", flag: "tw" },
+    { name: "Thailand", flag: "th" },
+    { name: "United States", flag: "us" },
   ];
 
   const regionList = [
-    "Americas", "Asia", "Asia 11 countries", "Asialink 7 countries",
-    "Australia & New Zealand", "EU 33 countries", "EU 40 countries", "Eurolink",
-    "Europe", "Europe 33 countries", "Hong Kong & Macau", "Middle East and Africa",
-    "North America & Canada", "North America A", "Oceania", "Singapore & Malaysia"
+    "Americas",
+    "Asia",
+    "Asia 11 countries",
+    "Asialink 7 countries",
+    "Australia & New Zealand",
+    "EU 33 countries",
+    "EU 40 countries",
+    "Eurolink",
+    "Europe",
+    "Europe 33 countries",
+    "Hong Kong & Macau",
+    "Middle East and Africa",
+    "North America & Canada",
+    "North America A",
+    "Oceania",
+    "Singapore & Malaysia",
   ];
 
   const guideItems = [
     { label: "What is eSIM", path: "/support" },
     { label: "Installation instructions", path: "/support" },
     { label: "Support", path: "/support" },
-    { label: "Order Tracking Search", path: "/support" }
+    { label: "Order Tracking Search", path: "/support" },
   ];
 
   const languages = [
-    { code: 'en', label: 'English', flag: 'us' },
-    { code: 'vi', label: 'Tiếng Việt', flag: 'vn' }
+    { code: "en", label: "English", flag: "us" },
+    { code: "vi", label: "Tiếng Việt", flag: "vn" },
   ];
 
   useEffect(() => {
     const handleScroll = () => {
       setIsScrolled(window.scrollY > 300);
     };
-    window.addEventListener('scroll', handleScroll);
-    return () => window.removeEventListener('scroll', handleScroll);
+    window.addEventListener("scroll", handleScroll);
+    return () => window.removeEventListener("scroll", handleScroll);
   }, []);
 
   const handleCountryClick = (c: { name: string; flag: string }) => {
-    navigate(`/product/${c.name.toLowerCase()}`, { state: { country: c.name, flag: c.flag } });
+    navigate(`/product/${c.name.toLowerCase()}`, {
+      state: { country: c.name, flag: c.flag },
+    });
     setIsBuySimMegaVisible(false);
     setIsMenuOpen(false);
   };
 
   const handleRegionClick = (region: string) => {
-    navigate(`/product/${region.toLowerCase()}`, { state: { country: region, flag: 'un' } });
+    navigate(`/product/${region.toLowerCase()}`, {
+      state: { country: region, flag: "un" },
+    });
     setIsBuySimMegaVisible(false);
     setIsMenuOpen(false);
   };
 
   useEffect(() => {
     const handleClickOutside = (event: MouseEvent) => {
-      if (langMenuRef.current && !langMenuRef.current.contains(event.target as Node)) {
+      if (
+        langMenuRef.current &&
+        !langMenuRef.current.contains(event.target as Node)
+      ) {
         setIsLangMenuOpen(false);
       }
     };
-    document.addEventListener('mousedown', handleClickOutside);
-    return () => document.removeEventListener('mousedown', handleClickOutside);
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => document.removeEventListener("mousedown", handleClickOutside);
   }, []);
 
   useEffect(() => {
     const handleResize = () => {
       if (window.innerWidth >= 1024) setIsMenuOpen(false);
     };
-    window.addEventListener('resize', handleResize);
-    return () => window.removeEventListener('resize', handleResize);
+    window.addEventListener("resize", handleResize);
+    return () => window.removeEventListener("resize", handleResize);
   }, []);
 
-  const currentLangObj = languages.find(l => l.code === selectedLang) || languages[0];
+  const currentLangObj =
+    languages.find((l) => l.code === selectedLang) || languages[0];
 
   const isActive = (path: string) => location.pathname === path;
 
@@ -97,96 +119,184 @@ const Header: React.FC = () => {
             {/* Logo */}
             <Link to="/" className="flex-shrink-0 flex items-center">
               <div className="flex items-center space-x-1">
-                <svg className="w-8 h-8 text-[#EE0434]" viewBox="0 0 24 24" fill="currentColor">
+                {/* <svg className="w-8 h-8 text-[#EE0434]" viewBox="0 0 24 24" fill="currentColor">
                   <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
-                </svg>
-                <span className="text-2xl font-bold tracking-tighter">
+                </svg> */}
+                <img src={logo} alt="InfiGate Logo" className="w-35" />
+                {/* <span className="text-2xl font-bold tracking-tighter">
                   <span className="text-[#EE0434]">Infi</span>
                   <span className="text-[#333]">Gate</span>
-                </span>
+                </span> */}
               </div>
             </Link>
 
             {/* Desktop Search on Scroll */}
-            <div className={`hidden lg:flex items-center transition-all duration-500 overflow-hidden ${isScrolled ? 'flex-1 max-w-md mx-8 opacity-100' : 'max-w-0 opacity-0 pointer-events-none'}`}>
-               <div className="relative w-full">
-                 <input 
-                   type="text" 
-                   placeholder="Search country..." 
-                   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"
-                 />
-                 <svg className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
-               </div>
+            <div
+              className={`hidden lg:flex items-center transition-all duration-500 overflow-hidden ${
+                isScrolled
+                  ? "flex-1 max-w-md mx-8 opacity-100"
+                  : "max-w-0 opacity-0 pointer-events-none"
+              }`}
+            >
+              <div className="relative w-full">
+                <input
+                  type="text"
+                  placeholder="Search country..."
+                  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"
+                />
+                <svg
+                  className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2.5}
+                    d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
+                  />
+                </svg>
+              </div>
             </div>
 
             {/* Desktop Nav */}
-            <nav className={`hidden lg:flex items-center h-full transition-all duration-300 ${isScrolled ? 'space-x-4' : 'space-x-8'}`}>
-              <Link to="/" className={`text-[17px] font-bold ${isActive('/') ? 'text-[#EE0434]' : 'text-slate-700 hover:text-[#EE0434]'}`}>Home</Link>
-              
-              <div 
+            <nav
+              className={`hidden lg:flex items-center h-full transition-all duration-300 ${
+                isScrolled ? "space-x-4" : "space-x-8"
+              }`}
+            >
+              <Link
+                to="/"
+                className={`text-[17px] font-bold ${
+                  isActive("/")
+                    ? "text-[#EE0434]"
+                    : "text-slate-700 hover:text-[#EE0434]"
+                }`}
+              >
+                Home
+              </Link>
+
+              <div
                 className="relative h-full flex items-center"
                 onMouseEnter={() => setIsBuySimMegaVisible(true)}
                 onMouseLeave={() => setIsBuySimMegaVisible(false)}
               >
-                <Link 
+                <Link
                   to="/buy-sim"
-                  className={`flex items-center text-[17px] font-bold transition-colors ${isActive('/buy-sim') ? 'text-[#EE0434]' : 'text-slate-700 hover:text-[#EE0434]'}`}
+                  className={`flex items-center text-[17px] font-bold transition-colors ${
+                    isActive("/buy-sim")
+                      ? "text-[#EE0434]"
+                      : "text-slate-700 hover:text-[#EE0434]"
+                  }`}
                 >
-                  Buy SIM <svg className={`ml-1 w-4 h-4 transition-transform ${isBuySimMegaVisible ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
+                  Buy SIM{" "}
+                  <svg
+                    className={`ml-1 w-4 h-4 transition-transform ${
+                      isBuySimMegaVisible ? "rotate-180" : ""
+                    }`}
+                    fill="none"
+                    stroke="currentColor"
+                    viewBox="0 0 24 24"
+                  >
+                    <path
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
+                      strokeWidth={2}
+                      d="M19 9l-7 7-7-7"
+                    />
+                  </svg>
                 </Link>
 
                 {isBuySimMegaVisible && (
                   <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">
                     <div className="w-[280px] bg-red-50 p-10 flex flex-col">
-                      <h3 className="text-4xl font-black text-slate-900 mb-4">Buy SIM</h3>
-                      <button 
+                      <h3 className="text-4xl font-black text-slate-900 mb-4">
+                        Buy SIM
+                      </h3>
+                      <button
                         onClick={() => {
-                          navigate('/buy-sim');
+                          navigate("/buy-sim");
                           setIsBuySimMegaVisible(false);
                         }}
                         className="text-[#EE0434] font-bold text-xl flex items-center group mb-8"
                       >
-                        View all <svg className="ml-2 w-5 h-5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M9 5l7 7-7 7" /></svg>
+                        View all{" "}
+                        <svg
+                          className="ml-2 w-5 h-5 transition-transform group-hover:translate-x-1"
+                          fill="none"
+                          stroke="currentColor"
+                          viewBox="0 0 24 24"
+                        >
+                          <path
+                            strokeLinecap="round"
+                            strokeLinejoin="round"
+                            strokeWidth={3}
+                            d="M9 5l7 7-7 7"
+                          />
+                        </svg>
                       </button>
                     </div>
                     <div className="flex-1 p-10">
                       <div className="flex space-x-4 mb-10">
-                        <button 
-                          onClick={() => setActiveDesktopTab('popular')} 
-                          className={`px-8 py-3 rounded-full text-base font-bold transition-all ${activeDesktopTab === 'popular' ? 'bg-[#EE0434] text-white shadow-lg shadow-red-100' : 'bg-slate-50 text-slate-900 hover:bg-slate-100'}`}
+                        <button
+                          onClick={() => setActiveDesktopTab("popular")}
+                          className={`px-8 py-3 rounded-full text-base font-bold transition-all ${
+                            activeDesktopTab === "popular"
+                              ? "bg-[#EE0434] text-white shadow-lg shadow-red-100"
+                              : "bg-slate-50 text-slate-900 hover:bg-slate-100"
+                          }`}
                         >
                           Most Popular
                         </button>
-                        <button 
-                          onClick={() => setActiveDesktopTab('region')} 
-                          className={`px-8 py-3 rounded-full text-base font-bold transition-all ${activeDesktopTab === 'region' ? 'bg-[#EE0434] text-white shadow-lg shadow-red-100' : 'bg-slate-50 text-slate-900 hover:bg-slate-100'}`}
+                        <button
+                          onClick={() => setActiveDesktopTab("region")}
+                          className={`px-8 py-3 rounded-full text-base font-bold transition-all ${
+                            activeDesktopTab === "region"
+                              ? "bg-[#EE0434] text-white shadow-lg shadow-red-100"
+                              : "bg-slate-50 text-slate-900 hover:bg-slate-100"
+                          }`}
                         >
                           Region
                         </button>
                       </div>
 
-                      {activeDesktopTab === 'popular' ? (
+                      {activeDesktopTab === "popular" ? (
                         <div className="grid grid-cols-4 gap-y-8 gap-x-4">
                           {countries.map((c) => (
-                            <div key={c.name} onClick={() => handleCountryClick(c)} className="flex items-center space-x-3 group cursor-pointer hover:bg-slate-50 p-2 rounded-xl transition-colors">
+                            <div
+                              key={c.name}
+                              onClick={() => handleCountryClick(c)}
+                              className="flex items-center space-x-3 group cursor-pointer hover:bg-slate-50 p-2 rounded-xl transition-colors"
+                            >
                               <div className="w-7 h-7 rounded-full overflow-hidden border border-slate-200 shadow-sm shrink-0">
-                                 <img src={`https://flagcdn.com/w40/${c.flag}.png`} alt={c.name} className="w-full h-full object-cover" />
+                                <img
+                                  src={`https://flagcdn.com/w40/${c.flag}.png`}
+                                  alt={c.name}
+                                  className="w-full h-full object-cover"
+                                />
                               </div>
-                              <span className="text-[16px] font-bold text-slate-700 group-hover:text-[#EE0434] transition-colors">{c.name}</span>
+                              <span className="text-[16px] font-bold text-slate-700 group-hover:text-[#EE0434] transition-colors">
+                                {c.name}
+                              </span>
                             </div>
                           ))}
                         </div>
                       ) : (
                         <div className="grid grid-cols-4 gap-y-6 gap-x-2">
                           {regionList.map((region) => (
-                            <div 
-                              key={region} 
-                              onClick={() => handleRegionClick(region)} 
+                            <div
+                              key={region}
+                              onClick={() => handleRegionClick(region)}
                               className="flex items-center space-x-3 group cursor-pointer hover:bg-slate-50 p-2 rounded-xl transition-colors min-w-0"
                             >
                               <div className="w-7 h-7 rounded-full bg-red-50 flex items-center justify-center shrink-0 border border-red-100/50">
-                                <svg className="w-4 h-4 text-red-300" fill="currentColor" viewBox="0 0 24 24">
-                                  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7.01-3.55-7.5-7.5H7v-1H3.5c.49-3.95 3.55-7.01 7.5-7.5v3.5h1V3.5c3.95.49 7.01 3.55 7.5 7.5H17v1h3.5c-.49 3.95-3.55-7.01-7.5 7.5v-3.5h-1v3.5z"/>
+                                <svg
+                                  className="w-4 h-4 text-red-300"
+                                  fill="currentColor"
+                                  viewBox="0 0 24 24"
+                                >
+                                  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7.01-3.55-7.5-7.5H7v-1H3.5c.49-3.95 3.55-7.01 7.5-7.5v3.5h1V3.5c3.95.49 7.01 3.55 7.5 7.5H17v1h3.5c-.49 3.95-3.55-7.01-7.5 7.5v-3.5h-1v3.5z" />
                                 </svg>
                               </div>
                               <span className="text-[16px] font-bold text-slate-700 group-hover:text-[#EE0434] transition-colors truncate">
@@ -201,24 +311,43 @@ const Header: React.FC = () => {
                 )}
               </div>
 
-              <div 
+              <div
                 className="relative h-full flex items-center"
                 onMouseEnter={() => setIsGuideMegaVisible(true)}
                 onMouseLeave={() => setIsGuideMegaVisible(false)}
               >
-                <Link 
+                <Link
                   to="/support"
-                  className={`flex items-center text-[17px] font-bold transition-colors ${isActive('/support') ? 'text-[#EE0434]' : 'text-slate-700 hover:text-[#EE0434]'}`}
+                  className={`flex items-center text-[17px] font-bold transition-colors ${
+                    isActive("/support")
+                      ? "text-[#EE0434]"
+                      : "text-slate-700 hover:text-[#EE0434]"
+                  }`}
                 >
-                  Guide <svg className={`ml-1 w-4 h-4 transition-transform ${isGuideMegaVisible ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
+                  Guide{" "}
+                  <svg
+                    className={`ml-1 w-4 h-4 transition-transform ${
+                      isGuideMegaVisible ? "rotate-180" : ""
+                    }`}
+                    fill="none"
+                    stroke="currentColor"
+                    viewBox="0 0 24 24"
+                  >
+                    <path
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
+                      strokeWidth={2}
+                      d="M19 9l-7 7-7-7"
+                    />
+                  </svg>
                 </Link>
 
                 {isGuideMegaVisible && (
                   <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">
                     <div className="flex-1 py-10 px-6 flex flex-col">
                       {guideItems.map((item, idx) => (
-                        <button 
-                          key={item.label} 
+                        <button
+                          key={item.label}
                           onClick={() => {
                             navigate(item.path);
                             setIsGuideMegaVisible(false);
@@ -233,16 +362,24 @@ const Header: React.FC = () => {
                 )}
               </div>
 
-              <Link 
+              <Link
                 to="/news"
-                className={`text-[17px] font-bold transition-colors ${isActive('/news') ? 'text-[#EE0434]' : 'text-slate-700 hover:text-[#EE0434]'}`}
+                className={`text-[17px] font-bold transition-colors ${
+                  isActive("/news")
+                    ? "text-[#EE0434]"
+                    : "text-slate-700 hover:text-[#EE0434]"
+                }`}
               >
                 News
               </Link>
-              
-              <Link 
+
+              <Link
                 to="/contact"
-                className={`text-[17px] font-bold transition-colors ${isActive('/contact') ? 'text-[#EE0434]' : 'text-slate-700 hover:text-[#EE0434]'}`}
+                className={`text-[17px] font-bold transition-colors ${
+                  isActive("/contact")
+                    ? "text-[#EE0434]"
+                    : "text-slate-700 hover:text-[#EE0434]"
+                }`}
               >
                 Contact
               </Link>
@@ -250,26 +387,52 @@ const Header: React.FC = () => {
 
             {/* Icons */}
             <div className="flex items-center space-x-5">
-              <Link 
+              <Link
                 to="/login"
                 className="p-2 text-slate-700 hover:text-[#EE0434] transition-colors"
               >
-                <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
+                <svg
+                  className="w-6 h-6"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2}
+                    d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
+                  />
+                </svg>
               </Link>
               <button className="hidden sm:flex p-2 text-slate-700 hover:text-[#EE0434] relative">
-                <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" /></svg>
-                <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">0</span>
+                <svg
+                  className="w-6 h-6"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2}
+                    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"
+                  />
+                </svg>
+                <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">
+                  0
+                </span>
               </button>
 
               <div className="relative" ref={langMenuRef}>
-                <button 
+                <button
                   onClick={() => setIsLangMenuOpen(!isLangMenuOpen)}
                   className="bg-white p-1 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all flex items-center justify-center"
                 >
                   <div className="w-8 h-8 rounded-full overflow-hidden border border-slate-50">
-                    <img 
-                      src={`https://flagcdn.com/w80/${currentLangObj.flag}.png`} 
-                      alt={currentLangObj.label} 
+                    <img
+                      src={`https://flagcdn.com/w80/${currentLangObj.flag}.png`}
+                      alt={currentLangObj.label}
                       className="w-full h-full object-cover"
                     />
                   </div>
@@ -282,21 +445,29 @@ const Header: React.FC = () => {
                         <button
                           key={lang.code}
                           onClick={() => {
-                            setSelectedLang(lang.code as 'en' | 'vi');
+                            setSelectedLang(lang.code as "en" | "vi");
                             setIsLangMenuOpen(false);
                           }}
                           className={`flex items-center space-x-3 px-5 py-4 w-full text-left transition-colors ${
-                            selectedLang === lang.code ? 'bg-slate-50' : 'hover:bg-slate-50/50'
+                            selectedLang === lang.code
+                              ? "bg-slate-50"
+                              : "hover:bg-slate-50/50"
                           }`}
                         >
                           <div className="w-7 h-7 rounded-full overflow-hidden border border-slate-100 shadow-sm">
-                            <img 
-                              src={`https://flagcdn.com/w80/${lang.flag}.png`} 
-                              alt={lang.label} 
+                            <img
+                              src={`https://flagcdn.com/w80/${lang.flag}.png`}
+                              alt={lang.label}
                               className="w-full h-full object-cover"
                             />
                           </div>
-                          <span className={`text-[15px] font-bold ${selectedLang === lang.code ? 'text-slate-900' : 'text-slate-500'}`}>
+                          <span
+                            className={`text-[15px] font-bold ${
+                              selectedLang === lang.code
+                                ? "text-slate-900"
+                                : "text-slate-500"
+                            }`}
+                          >
                             {lang.label}
                           </span>
                         </button>
@@ -306,8 +477,23 @@ const Header: React.FC = () => {
                 )}
               </div>
 
-              <button onClick={() => setIsMenuOpen(true)} className="lg:hidden p-2 text-slate-900">
-                <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 6h16M4 12h16m-7 6h7" /></svg>
+              <button
+                onClick={() => setIsMenuOpen(true)}
+                className="lg:hidden p-2 text-slate-900"
+              >
+                <svg
+                  className="w-6 h-6"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2.5}
+                    d="M4 6h16M4 12h16m-7 6h7"
+                  />
+                </svg>
               </button>
             </div>
           </div>
@@ -315,12 +501,30 @@ const Header: React.FC = () => {
       </header>
 
       {/* Full-Screen Mobile Menu with Slide-Right Transition */}
-      <div className={`fixed inset-0 z-[100] lg:hidden transition-all duration-500 ease-in-out ${isMenuOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}>
-        <div className={`absolute inset-0 bg-white flex flex-col transform transition-transform duration-500 ease-out ${isMenuOpen ? 'translate-x-0' : '-translate-x-full'}`}>
+      <div
+        className={`fixed inset-0 z-[100] lg:hidden transition-all duration-500 ease-in-out ${
+          isMenuOpen
+            ? "opacity-100 pointer-events-auto"
+            : "opacity-0 pointer-events-none"
+        }`}
+      >
+        <div
+          className={`absolute inset-0 bg-white flex flex-col transform transition-transform duration-500 ease-out ${
+            isMenuOpen ? "translate-x-0" : "-translate-x-full"
+          }`}
+        >
           {/* Mobile Menu Header */}
           <div className="flex justify-between items-center p-6 border-b border-slate-50">
-            <Link to="/" onClick={() => setIsMenuOpen(false)} className="flex items-center space-x-1">
-              <svg className="w-7 h-7 text-[#EE0434]" viewBox="0 0 24 24" fill="currentColor">
+            <Link
+              to="/"
+              onClick={() => setIsMenuOpen(false)}
+              className="flex items-center space-x-1"
+            >
+              <svg
+                className="w-7 h-7 text-[#EE0434]"
+                viewBox="0 0 24 24"
+                fill="currentColor"
+              >
                 <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
               </svg>
               <span className="text-xl font-black tracking-tighter">
@@ -328,45 +532,97 @@ const Header: React.FC = () => {
                 <span className="text-[#333]">Gate</span>
               </span>
             </Link>
-            <button onClick={() => setIsMenuOpen(false)} className="p-2 text-slate-400 hover:text-[#EE0434] transition-colors rounded-full hover:bg-slate-100">
-              <svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
+            <button
+              onClick={() => setIsMenuOpen(false)}
+              className="p-2 text-slate-400 hover:text-[#EE0434] transition-colors rounded-full hover:bg-slate-100"
+            >
+              <svg
+                className="w-8 h-8"
+                fill="none"
+                stroke="currentColor"
+                viewBox="0 0 24 24"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M6 18L18 6M6 6l12 12"
+                />
+              </svg>
             </button>
           </div>
 
           <div className="flex-1 overflow-y-auto px-6 py-8 space-y-6">
             <div className="flex flex-col items-center space-y-4">
-              <Link 
-                to="/" 
+              <Link
+                to="/"
                 onClick={() => setIsMenuOpen(false)}
-                className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${isActive('/') ? 'bg-red-50 text-[#EE0434]' : 'text-slate-800 hover:bg-slate-50'}`}
+                className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
+                  isActive("/")
+                    ? "bg-red-50 text-[#EE0434]"
+                    : "text-slate-800 hover:bg-slate-50"
+                }`}
               >
                 Home
               </Link>
-              
+
               <div className="w-full">
-                <button 
+                <button
                   onClick={() => setIsBuySimExpanded(!isBuySimExpanded)}
-                  className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${isActive('/buy-sim') ? 'bg-red-50 text-[#EE0434]' : 'text-slate-800 hover:bg-slate-50'}`}
+                  className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
+                    isActive("/buy-sim")
+                      ? "bg-red-50 text-[#EE0434]"
+                      : "text-slate-800 hover:bg-slate-50"
+                  }`}
                 >
                   <span>Buy SIM</span>
-                  <svg className={`w-6 h-6 transition-transform duration-300 ${isBuySimExpanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 9l-7 7-7-7" /></svg>
+                  <svg
+                    className={`w-6 h-6 transition-transform duration-300 ${
+                      isBuySimExpanded ? "rotate-180" : ""
+                    }`}
+                    fill="none"
+                    stroke="currentColor"
+                    viewBox="0 0 24 24"
+                  >
+                    <path
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
+                      strokeWidth={2.5}
+                      d="M19 9l-7 7-7-7"
+                    />
+                  </svg>
                 </button>
-                <div className={`overflow-hidden transition-all duration-300 ease-in-out ${isBuySimExpanded ? 'max-h-[1000px] opacity-100 mt-4' : 'max-h-0 opacity-0'}`}>
+                <div
+                  className={`overflow-hidden transition-all duration-300 ease-in-out ${
+                    isBuySimExpanded
+                      ? "max-h-[1000px] opacity-100 mt-4"
+                      : "max-h-0 opacity-0"
+                  }`}
+                >
                   <div className="grid grid-cols-2 gap-3 px-2">
-                    <button 
-                      onClick={() => { navigate('/buy-sim'); setIsMenuOpen(false); }}
+                    <button
+                      onClick={() => {
+                        navigate("/buy-sim");
+                        setIsMenuOpen(false);
+                      }}
                       className="col-span-2 text-center py-4 bg-slate-50 rounded-2xl text-[#EE0434] font-black text-sm uppercase tracking-wider shadow-sm"
                     >
                       View All Destinations →
                     </button>
-                    {countries.map(c => (
-                      <button 
+                    {countries.map((c) => (
+                      <button
                         key={c.name}
                         onClick={() => handleCountryClick(c)}
                         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"
                       >
-                        <img src={`https://flagcdn.com/w80/${c.flag}.png`} alt={c.name} className="w-10 h-10 rounded-full object-cover border-2 border-slate-50" />
-                        <span className="text-sm font-bold text-slate-700">{c.name}</span>
+                        <img
+                          src={`https://flagcdn.com/w80/${c.flag}.png`}
+                          alt={c.name}
+                          className="w-10 h-10 rounded-full object-cover border-2 border-slate-50"
+                        />
+                        <span className="text-sm font-bold text-slate-700">
+                          {c.name}
+                        </span>
                       </button>
                     ))}
                   </div>
@@ -374,19 +630,46 @@ const Header: React.FC = () => {
               </div>
 
               <div className="w-full">
-                <button 
+                <button
                   onClick={() => setIsGuideExpanded(!isGuideExpanded)}
-                  className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${isActive('/support') ? 'bg-red-50 text-[#EE0434]' : 'text-slate-800 hover:bg-slate-50'}`}
+                  className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
+                    isActive("/support")
+                      ? "bg-red-50 text-[#EE0434]"
+                      : "text-slate-800 hover:bg-slate-50"
+                  }`}
                 >
                   <span>Guide</span>
-                  <svg className={`w-6 h-6 transition-transform duration-300 ${isGuideExpanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 9l-7 7-7-7" /></svg>
+                  <svg
+                    className={`w-6 h-6 transition-transform duration-300 ${
+                      isGuideExpanded ? "rotate-180" : ""
+                    }`}
+                    fill="none"
+                    stroke="currentColor"
+                    viewBox="0 0 24 24"
+                  >
+                    <path
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
+                      strokeWidth={2.5}
+                      d="M19 9l-7 7-7-7"
+                    />
+                  </svg>
                 </button>
-                <div className={`overflow-hidden transition-all duration-300 ease-in-out ${isGuideExpanded ? 'max-h-[400px] opacity-100 mt-4' : 'max-h-0 opacity-0'}`}>
+                <div
+                  className={`overflow-hidden transition-all duration-300 ease-in-out ${
+                    isGuideExpanded
+                      ? "max-h-[400px] opacity-100 mt-4"
+                      : "max-h-0 opacity-0"
+                  }`}
+                >
                   <div className="flex flex-col space-y-2 px-2">
-                    {guideItems.map(item => (
-                      <button 
+                    {guideItems.map((item) => (
+                      <button
                         key={item.label}
-                        onClick={() => { navigate(item.path); setIsMenuOpen(false); }}
+                        onClick={() => {
+                          navigate(item.path);
+                          setIsMenuOpen(false);
+                        }}
                         className="w-full text-center py-4 rounded-2xl bg-slate-50 text-slate-600 font-bold hover:text-[#EE0434] active:bg-red-50"
                       >
                         {item.label}
@@ -396,42 +679,60 @@ const Header: React.FC = () => {
                 </div>
               </div>
 
-              <Link 
-                to="/news" 
+              <Link
+                to="/news"
                 onClick={() => setIsMenuOpen(false)}
-                className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${isActive('/news') ? 'bg-red-50 text-[#EE0434]' : 'text-slate-800 hover:bg-slate-50'}`}
+                className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
+                  isActive("/news")
+                    ? "bg-red-50 text-[#EE0434]"
+                    : "text-slate-800 hover:bg-slate-50"
+                }`}
               >
                 News
               </Link>
-              
-              <Link 
-                to="/contact" 
+
+              <Link
+                to="/contact"
                 onClick={() => setIsMenuOpen(false)}
-                className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${isActive('/contact') ? 'bg-red-50 text-[#EE0434]' : 'text-slate-800 hover:bg-slate-50'}`}
+                className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
+                  isActive("/contact")
+                    ? "bg-red-50 text-[#EE0434]"
+                    : "text-slate-800 hover:bg-slate-50"
+                }`}
               >
                 Contact
               </Link>
 
               <div className="w-full pt-4">
-                 <p className="text-center text-slate-400 font-bold text-xs uppercase tracking-widest mb-4">Select Language</p>
-                 <div className="flex justify-center space-x-4">
-                    {languages.map(lang => (
-                      <button
-                        key={lang.code}
-                        onClick={() => setSelectedLang(lang.code as 'en' | 'vi')}
-                        className={`flex items-center space-x-2 px-4 py-2 rounded-2xl transition-all border ${selectedLang === lang.code ? 'bg-red-50 border-[#EE0434] text-[#EE0434]' : 'bg-white border-slate-100 text-slate-500'}`}
-                      >
-                        <img src={`https://flagcdn.com/w40/${lang.flag}.png`} alt={lang.label} className="w-6 h-6 rounded-full object-cover border border-slate-100" />
-                        <span className="font-bold">{lang.label}</span>
-                      </button>
-                    ))}
-                 </div>
+                <p className="text-center text-slate-400 font-bold text-xs uppercase tracking-widest mb-4">
+                  Select Language
+                </p>
+                <div className="flex justify-center space-x-4">
+                  {languages.map((lang) => (
+                    <button
+                      key={lang.code}
+                      onClick={() => setSelectedLang(lang.code as "en" | "vi")}
+                      className={`flex items-center space-x-2 px-4 py-2 rounded-2xl transition-all border ${
+                        selectedLang === lang.code
+                          ? "bg-red-50 border-[#EE0434] text-[#EE0434]"
+                          : "bg-white border-slate-100 text-slate-500"
+                      }`}
+                    >
+                      <img
+                        src={`https://flagcdn.com/w40/${lang.flag}.png`}
+                        alt={lang.label}
+                        className="w-6 h-6 rounded-full object-cover border border-slate-100"
+                      />
+                      <span className="font-bold">{lang.label}</span>
+                    </button>
+                  ))}
+                </div>
               </div>
             </div>
           </div>
 
           <div className="p-8 border-t border-slate-50 bg-slate-50/50">
-            <Link 
+            <Link
               to="/login"
               onClick={() => setIsMenuOpen(false)}
               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"

+ 10 - 7
EsimLao/esim-vite/src/components/TestimonialCard.tsx

@@ -1,21 +1,24 @@
 import React, { useState, useEffect, useCallback } from "react";
 import { TestimonialType } from "../services/types";
+import { Review } from "../services/content/types";
 
-const TestimonialCard: React.FC<{ item: TestimonialType }> = ({ item }) => (
+const TestimonialCard: React.FC<{ item: Review }> = ({ item }) => (
   <div className="bg-white/10 backdrop-blur-md border border-white/20 rounded-[24px] md:rounded-[32px] p-5 md:p-6 min-w-[280px] md:min-w-[320px] max-w-[340px] shadow-xl text-white flex flex-col space-y-4">
     <div className="flex items-center space-x-3">
       <img
-        src={item.avatar}
-        alt={item.name}
+        src={item.AvatarUrl}
+        alt={item.CustomerName}
         className="w-10 h-10 md:w-12 md:h-12 rounded-full border-2 border-white/50 object-cover"
       />
       <div>
-        <h4 className="font-bold text-sm md:text-lg">{item.name}</h4>
-        <p className="text-white/60 text-[10px] md:text-xs">{item.location}</p>
+        <h4 className="font-bold text-sm md:text-lg">{item.CustomerName}</h4>
+        <p className="text-white/60 text-[10px] md:text-xs">
+          {item.destination}
+        </p>
       </div>
     </div>
     <div className="flex space-x-1">
-      {[...Array(item.rating)].map((_, i) => (
+      {[...Array(item.Rating)].map((_, i) => (
         <svg
           key={i}
           className="w-3.5 h-3.5 md:w-4 md:h-4 text-yellow-400"
@@ -27,7 +30,7 @@ const TestimonialCard: React.FC<{ item: TestimonialType }> = ({ item }) => (
       ))}
     </div>
     <p className="text-xs md:text-sm font-medium leading-relaxed italic text-white/90 whitespace-normal">
-      "{item.content}"
+      "{item.reviewContent}"
     </p>
   </div>
 );

+ 15 - 11
EsimLao/esim-vite/src/components/TopLoader.tsx

@@ -1,19 +1,22 @@
 import React from "react";
+import { useAppSelector } from "../hooks/useRedux";
 
 const TopLoader: React.FC<{ visible: boolean }> = ({ visible }) => {
+  const isLoading = useAppSelector((state) => state.loading.isLoading);
   return (
-    <div
-      className={`fixed top-0 left-0 w-full h-[3px] z-[9999] pointer-events-none transition-opacity duration-500 ${
-        visible ? "opacity-100" : "opacity-0"
-      }`}
-    >
-      {/* Primary loading bar */}
-      <div className="absolute inset-0 bg-[#EE0434] shadow-[0_0_15px_rgba(238,4,52,0.6)] animate-[progress-fill_2s_infinite_ease-in-out]" />
+    isLoading && (
+      <div
+        className={`fixed top-0 left-0 w-full h-[3px] z-[9999] pointer-events-none transition-opacity duration-500 ${
+          visible ? "opacity-100" : "opacity-0"
+        }`}
+      >
+        {/* Primary loading bar */}
+        <div className="absolute inset-0 bg-[#EE0434] shadow-[0_0_15px_rgba(238,4,52,0.6)] animate-[progress-fill_2s_infinite_ease-in-out]" />
 
-      {/* Secondary shimmer effect for a more active feel */}
-      <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent w-1/2 animate-[shimmer-flow_1.5s_infinite_linear]" />
+        {/* Secondary shimmer effect for a more active feel */}
+        <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent w-1/2 animate-[shimmer-flow_1.5s_infinite_linear]" />
 
-      <style>{`
+        <style>{`
         @keyframes progress-fill {
           0% { width: 0%; left: 0%; }
           50% { width: 70%; left: 15%; }
@@ -24,7 +27,8 @@ const TopLoader: React.FC<{ visible: boolean }> = ({ visible }) => {
           100% { transform: translateX(200%); }
         }
       `}</style>
-    </div>
+      </div>
+    )
   );
 };
 

+ 32 - 0
EsimLao/esim-vite/src/features/account/accuntSlice.ts

@@ -0,0 +1,32 @@
+import { AccountInfo } from "../../services/auth/types";
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+const accountSlice = createSlice({
+  name: "account",
+  initialState: {
+    account: null,
+  },
+  reducers: {
+    accountLogin: (state, action: PayloadAction<AccountInfo>) => {
+      localStorage.setItem("token", action.payload.accessToken);
+      localStorage.setItem("refreshToken", action.payload.refreshToken);
+      localStorage.setItem("accountInfo", JSON.stringify(action.payload));
+      return {
+        ...state,
+        account: action.payload,
+      };
+    },
+    accountLogout: (state) => {
+      localStorage.removeItem("token");
+      localStorage.removeItem("refreshToken");
+      localStorage.removeItem("accountInfo");
+      return {
+        ...state,
+        account: null,
+      };
+    },
+  },
+});
+
+export const { accountLogin, accountLogout } = accountSlice.actions;
+export default accountSlice.reducer;

+ 32 - 18
EsimLao/esim-vite/src/features/loading/loadingSlice.ts

@@ -1,23 +1,37 @@
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
 
 const loadingSlice = createSlice({
-    name: 'loading',
-    initialState: {
-        isLoading: false,
-        message: 'Loading ...',
+  name: "loading",
+  initialState: {
+    isLoading: false,
+    isSmallLoading: false,
+    message: "Loading ...",
+  },
+  reducers: {
+    startLoading: (state, action: PayloadAction<any>) => {
+      console.log("Setting animation enabled state to: ", action.payload);
+      return {
+        ...state,
+        isLoading: true,
+        message: action.payload?.message ?? "Loading ...",
+      };
     },
-    reducers: {
-        startLoading: (state, action: PayloadAction<any>) => {
-            console.log("Setting animation enabled state to: ", action.payload);
-            return {
-                ...state,
-                isLoading: true,
-                message: action.payload?.message ?? 'Loading ...',
-            };
-        },
-        stopLoading: state => { state.isLoading = false }
-    }
+    stopLoading: (state) => {
+      state.isLoading = false;
+    },
+    startSmallLoading: (state) => {
+      state.isSmallLoading = true;
+    },
+    stopSmallLoading: (state) => {
+      state.isSmallLoading = false;
+    },
+  },
 });
 
-export const { startLoading, stopLoading } = loadingSlice.actions;
-export default loadingSlice.reducer;
+export const {
+  startLoading,
+  stopLoading,
+  startSmallLoading,
+  stopSmallLoading,
+} = loadingSlice.actions;
+export default loadingSlice.reducer;

+ 16 - 0
EsimLao/esim-vite/src/global/constants.ts

@@ -0,0 +1,16 @@
+export const smallStaleTime = 1000 * 60; // 1 minute
+export const staleTime = 1000 * 60 * 5; // 5 minutes
+export const staleTimeInfinity = Infinity;
+export const userLoginAction = "login";
+export const userLogoutAction = "logout";
+
+export const timeDeplayAfterAnswer = 5;
+
+export const DataCacheKey = {
+  AREAS: "areas",
+  CATEGORIES: "categories",
+  ARTICLES: "articles",
+  BANNERS: "banners",
+  REVIEWS: "reviews",
+  FAQS: "faqs",
+};

+ 1 - 0
EsimLao/esim-vite/src/index.css

@@ -0,0 +1 @@
+@import "tailwindcss";

+ 43 - 35
EsimLao/esim-vite/src/pages/home/components/HomeBanner.tsx

@@ -1,41 +1,47 @@
+import { useAppDispatch } from "../../../hooks/useRedux";
+import {
+  startLoading,
+  stopLoading,
+} from "../../../features/loading/loadingSlice";
+import { DataCacheKey, staleTime } from "../../../global/constants";
+import { Banner } from "../../../services/content/types";
+import { useQuery } from "@tanstack/react-query";
 import React, { useState, useEffect, useCallback } from "react";
+import { contentApi } from "../../../apis/contentApi";
 
 const HomeBanner = () => {
   const [currentBanner, setCurrentBanner] = useState(0);
+  const dispatch = useAppDispatch();
 
-  const banners = [
-    {
-      badge: "Global Connectivity",
-      title: "Travel Smarter",
-      highlight: "With InfiGate",
-      desc: "High-speed 5G/4G connectivity across 200+ countries. No roaming fees, just instant access.",
-      gradient: "from-[#EE0434] via-[#cc002d] to-[#ff4d6d]",
-      image:
-        "https://images.unsplash.com/photo-1530789253588-583c8d9c03b0?q=80&w=1000&auto=format&fit=crop",
-    },
-    {
-      badge: "Digital Nomads",
-      title: "Ultra-Fast 5G",
-      highlight: "Work Anywhere",
-      desc: "Stay productive with stable high-speed data for meetings and remote work.",
-      gradient: "from-[#ff4d6d] via-[#EE0434] to-[#80001a]",
-      image:
-        "https://images.unsplash.com/photo-1488190211105-8b0e65b80b4e?q=80&w=1000&auto=format&fit=crop",
-    },
-    {
-      badge: "Exclusive Offers",
-      title: "Asian Tours",
-      highlight: "Special Deals",
-      desc: "Plan your next trip with our local-rate eSIMs starting from just $0.85/day.",
-      gradient: "from-[#EE0434] via-[#ff6b6b] to-[#c92a2a]",
-      image:
-        "https://images.unsplash.com/photo-1493976040374-85c8e12f0c0e?q=80&w=1000&auto=format&fit=crop",
+  const { data: loadBannerData = [] } = useQuery<Banner[]>({
+    queryKey: [DataCacheKey.BANNERS],
+    queryFn: async (): Promise<Banner[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await contentApi.LoadBanner({
+          pageNumber: 0,
+          pageSize: 10,
+        });
+        return res.data.banners as Banner[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
     },
+    staleTime: staleTime,
+  });
+
+  const gradients = [
+    "from-[#EE0434] via-[#cc002d] to-[#ff4d6d]",
+    "from-[#ff4d6d] via-[#EE0434] to-[#80001a]",
+    "from-[#EE0434] via-[#ff6b6b] to-[#c92a2a]",
   ];
 
   const nextBanner = useCallback(() => {
-    setCurrentBanner((prev) => (prev + 1) % banners.length);
-  }, [banners.length]);
+    setCurrentBanner((prev) => (prev + 1) % loadBannerData.length);
+  }, [loadBannerData.length]);
 
   useEffect(() => {
     const timer = setInterval(nextBanner, 6000);
@@ -45,7 +51,7 @@ const HomeBanner = () => {
   return (
     <section className="px-4 py-4 md:py-6 relative">
       <div className="max-w-7xl mx-auto relative overflow-hidden rounded-[24px] md:rounded-[48px] min-h-[400px] md:min-h-[550px] shadow-2xl bg-slate-900">
-        {banners.map((banner, idx) => (
+        {loadBannerData.map((banner, idx) => (
           <div
             key={idx}
             className={`absolute inset-0 w-full h-full transition-all duration-[1000ms] ease-in-out flex items-center ${
@@ -55,7 +61,9 @@ const HomeBanner = () => {
             }`}
           >
             <div
-              className={`absolute inset-0 bg-gradient-to-br ${banner.gradient} opacity-90 transition-all`}
+              className={`absolute inset-0 bg-gradient-to-br ${
+                gradients[idx % gradients.length]
+              } opacity-90 transition-all`}
             ></div>
 
             <div className="relative z-30 flex flex-col items-center justify-center p-6 md:p-24 w-full h-full text-center">
@@ -67,16 +75,16 @@ const HomeBanner = () => {
                 }`}
               >
                 <div className="inline-block px-4 py-1.5 bg-white/20 backdrop-blur-xl rounded-full text-[10px] md:text-xs font-black uppercase tracking-[0.3em] border border-white/10">
-                  {banner.badge}
+                  {banner.title}
                 </div>
                 <h1 className="text-4xl md:text-7xl lg:text-8xl font-black leading-[1.1] tracking-tighter max-w-4xl">
                   {banner.title} <br />
                   <span className="text-white/60 drop-shadow-sm">
-                    {banner.highlight}
+                    {banner.subtitle}
                   </span>
                 </h1>
                 <p className="text-sm md:text-xl text-white/80 font-medium max-w-2xl mx-auto leading-relaxed hidden sm:block">
-                  {banner.desc}
+                  {banner.title}
                 </p>
               </div>
             </div>
@@ -84,7 +92,7 @@ const HomeBanner = () => {
         ))}
 
         <div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-40 flex items-center space-x-4">
-          {banners.map((_, i) => (
+          {loadBannerData.map((_, i) => (
             <button
               key={i}
               onClick={() => setCurrentBanner(i)}

+ 49 - 20
EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx

@@ -1,26 +1,55 @@
+import { Faq } from "../../../services/content/types";
+import { DataCacheKey, staleTime } from "../../../global/constants";
+import { useQuery } from "@tanstack/react-query";
 import React, { useState, useEffect, useCallback } from "react";
+import { contentApi } from "../../../apis/contentApi";
+import {
+  startLoading,
+  stopLoading,
+} from "../../../features/loading/loadingSlice";
+import { useAppDispatch } from "../../../hooks/useRedux";
 
 const HomeFaq = () => {
   const [openFaqIndex, setOpenFaqIndex] = useState<number | null>(0);
+  const dispatch = useAppDispatch();
+  // const faqs = [
+  //   {
+  //     question:
+  //       "1. Does using an eSIM/ SIM incur any additional fees or services?",
+  //     answer:
+  //       "No. The price shown in the order already includes all costs, and no additional fees will arise during usage.",
+  //   },
+  //   {
+  //     question: "2. I received the email but there is no QR code.",
+  //     answer:
+  //       "Please check your spam or junk folder first. If it's not there, contact our support team via Zalo OA or WhatsApp with your order ID, and we will assist you in resending the QR code manually.",
+  //   },
+  //   {
+  //     question: "3. I lost my SIM while traveling abroad. Can it be reissued?",
+  //     answer:
+  //       "For eSIMs, we can easily resend the digital profile to your registered email. For physical SIMs, reissuance while abroad may be difficult due to shipping; we recommend switching to an eSIM if your device supports it.",
+  //   },
+  // ];
 
-  const faqs = [
-    {
-      question:
-        "1. Does using an eSIM/ SIM incur any additional fees or services?",
-      answer:
-        "No. The price shown in the order already includes all costs, and no additional fees will arise during usage.",
+  const { data: loadFaqsData = [] } = useQuery<Faq[]>({
+    queryKey: [DataCacheKey.FAQS],
+    queryFn: async (): Promise<Faq[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await contentApi.LoadFaq({
+          pageNumber: 0,
+          pageSize: 10,
+        });
+        return res.data.faqs as Faq[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
     },
-    {
-      question: "2. I received the email but there is no QR code.",
-      answer:
-        "Please check your spam or junk folder first. If it's not there, contact our support team via Zalo OA or WhatsApp with your order ID, and we will assist you in resending the QR code manually.",
-    },
-    {
-      question: "3. I lost my SIM while traveling abroad. Can it be reissued?",
-      answer:
-        "For eSIMs, we can easily resend the digital profile to your registered email. For physical SIMs, reissuance while abroad may be difficult due to shipping; we recommend switching to an eSIM if your device supports it.",
-    },
-  ];
+    staleTime: staleTime,
+  });
 
   const toggleFaq = (index: number) => {
     setOpenFaqIndex(openFaqIndex === index ? null : index);
@@ -40,7 +69,7 @@ const HomeFaq = () => {
           </div>
 
           <div className="space-y-4">
-            {faqs.map((faq, index) => (
+            {loadFaqsData.map((faq, index) => (
               <div key={index} className="border-b border-slate-100">
                 <button
                   onClick={() => toggleFaq(index)}
@@ -53,7 +82,7 @@ const HomeFaq = () => {
                         : "text-slate-800 group-hover:text-[#EE0434]"
                     }`}
                   >
-                    {faq.question}
+                    faq.question
                   </span>
                   <div
                     className={`shrink-0 w-6 h-6 md:w-8 md:h-8 flex items-center justify-center transition-all duration-500 ${
@@ -86,7 +115,7 @@ const HomeFaq = () => {
                 >
                   <div className="overflow-hidden">
                     <p className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8">
-                      {faq.answer}
+                      faq.answer
                     </p>
                   </div>
                 </div>

+ 34 - 102
EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx

@@ -1,6 +1,6 @@
 import { useAppDispatch, useAppSelector } from "../../../hooks/useRedux";
 import { SimProduct } from "@/src/services/types";
-import { useMutation } from "@tanstack/react-query";
+import { useMutation, useQuery } from "@tanstack/react-query";
 import React, { useState, useEffect, useCallback } from "react";
 import { useNavigate } from "react-router-dom";
 import { productApi } from "../../../apis/productApi";
@@ -8,114 +8,44 @@ import {
   startLoading,
   stopLoading,
 } from "../../../features/loading/loadingSlice";
+import { DataCacheKey, staleTime } from "../../../global/constants";
+import { AreaData } from "@/src/services/product/type";
 
 const HomeProduct = () => {
-  const areas = useAppSelector((state) => state.areas.areas);
   const [activeTab, setActiveTab] = useState<"country" | "region">("country");
   const navigate = useNavigate();
 
   const dispatch = useAppDispatch();
 
-  const products: SimProduct[] = [
-    {
-      id: "1",
-      country: "China",
-      flag: "cn",
-      originalPrice: "$0.94",
-      discountPrice: "$0.85",
-      discountLabel: "-10%",
-    },
-    {
-      id: "2",
-      country: "Hong Kong",
-      flag: "hk",
-      originalPrice: "$4.43",
-      discountPrice: "$4.21",
-      discountLabel: "-5%",
-    },
-    {
-      id: "3",
-      country: "Japan",
-      flag: "jp",
-      originalPrice: "$1.44",
-      discountPrice: "$1.3",
-      discountLabel: "-10%",
-    },
-    {
-      id: "4",
-      country: "Singapore",
-      flag: "sg",
-      originalPrice: "$4.43",
-      discountPrice: "$4.21",
-      discountLabel: "-5%",
-    },
-    {
-      id: "5",
-      country: "South Korea",
-      flag: "kr",
-      originalPrice: "$0.94",
-      discountPrice: "$0.85",
-      discountLabel: "-10%",
-    },
-    {
-      id: "6",
-      country: "Taiwan",
-      flag: "tw",
-      originalPrice: "$2.04",
-      discountPrice: "$1.84",
-      discountLabel: "-10%",
-    },
-    {
-      id: "7",
-      country: "Thailand",
-      flag: "th",
-      originalPrice: "$1.89",
-      discountPrice: "$1.78",
-      discountLabel: "-6%",
-    },
-    {
-      id: "8",
-      country: "United States",
-      flag: "us",
-      originalPrice: "$2.16",
-      discountPrice: "$1.94",
-      discountLabel: "-10%",
-    },
-  ];
-
-  useEffect(() => {
-    if (!areas || areas.length === 0) loadAreaApi.mutate();
-  }, []);
+  useEffect(() => {}, []);
 
-  const loadAreaApi = useMutation({
-    mutationFn: async () => {
-      dispatch(startLoading({}));
-      const res = await productApi.loadArea();
-      console.log("Load areas response:", res);
-      return res;
-    },
-    onSuccess: (data) => {
-      dispatch(stopLoading());
-      console.log("Get config response data:", data);
-      if (data && data.errorCode === 0) {
-        console.log("Get config successful");
-      } else {
-        console.error("Get config failed, no token received");
+  const { data: loadAreaData = [] } = useQuery<AreaData[]>({
+    queryKey: [DataCacheKey.AREAS],
+    queryFn: async (): Promise<AreaData[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await productApi.loadArea({
+          isCountry: "-1",
+          isPopular: "-1",
+        });
+        return res.data as AreaData[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
       }
     },
-    onError: (error: any) => {
-      dispatch(stopLoading());
-      console.error("Get config error:", error.response.data);
-    },
+    staleTime: staleTime,
   });
 
-  const handleProductClick = (p: SimProduct) => {
-    navigate(`/product/${p.country.toLowerCase()}`, {
+  const handleProductClick = (p: AreaData) => {
+    navigate(`/product/${p.areaName1.toLowerCase()}`, {
       state: {
-        country: p.country,
-        flag: p.flag,
+        country: p.areaName1,
+        flag: p.iconUrl,
         image:
-          p.country === "Thailand"
+          p.areaName1 === "Thailand"
             ? "https://images.unsplash.com/photo-1528181304800-2f140819898f?q=80&w=1000&auto=format&fit=crop"
             : `https://images.unsplash.com/photo-1526772662000-3f88f10c053b?q=80&w=1000&auto=format&fit=crop`,
       },
@@ -157,30 +87,32 @@ const HomeProduct = () => {
 
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
         <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-8">
-          {products.map((p) => (
+          {loadAreaData.map((p) => (
             <div
               key={p.id}
               onClick={() => handleProductClick(p)}
               className="group bg-white border border-slate-100 rounded-[32px] p-6 md:p-10 shadow-sm hover:shadow-2xl transition-all relative overflow-hidden cursor-pointer flex flex-col items-center"
             >
               <div className="absolute top-4 right-4 bg-[#EE0434] text-white text-[10px] font-black px-2.5 py-1 rounded-lg z-10">
-                {p.discountLabel}
+                {p.promotionPercent}%
               </div>
               <div className="w-16 h-16 md:w-20 md:h-20 mb-6 rounded-full overflow-hidden shadow-xl border-4 border-white group-hover:scale-110 transition-transform">
                 <img
-                  src={`https://flagcdn.com/w160/${p.flag}.png`}
-                  alt={p.country}
+                  src={`${p.iconUrl}`}
+                  alt={p.areaName1}
                   className="w-full h-full object-cover scale-150"
                 />
               </div>
               <h3 className="text-lg md:text-xl font-black text-slate-800 mb-2 truncate w-full group-hover:text-[#EE0434] transition-colors">
-                {p.country}
+                {p.areaName1}
               </h3>
               <p className="text-xs text-slate-400 line-through mb-1">
-                {p.originalPrice}
+                {p.curency}
+                {p.minSellPrice}
               </p>
               <p className="text-lg md:text-2xl font-black text-[#EE0434]">
-                From {p.discountPrice}
+                From {p.curency}
+                {p.minDisplayPrice}
               </p>
             </div>
           ))}

+ 88 - 54
EsimLao/esim-vite/src/pages/home/components/HomeTestimonial.tsx

@@ -1,56 +1,87 @@
+import { Review } from "../../../services/content/types";
 import TestimonialCard from "../../../components/TestimonialCard";
+import { useQuery } from "@tanstack/react-query";
+import { DataCacheKey, staleTime } from "../../../global/constants";
+import {
+  startLoading,
+  stopLoading,
+} from "../../../features/loading/loadingSlice";
+import { contentApi } from "../../../apis/contentApi";
+import { useAppDispatch } from "../../../hooks/useRedux";
 
 const HomeTestimonial = () => {
-  const testimonials = [
-    {
-      name: "Quang Huy",
-      location: "Nghệ An",
-      rating: 5,
-      avatar: "https://i.pravatar.cc/150?u=huy",
-      content:
-        "Trong suốt hành trình đi phượt bên Mỹ, Huy dò đường bằng Google Map phát trực tiếp từ sim data tốc độ cao mua sẵn ở Việt Nam tại shop infigate. Huy chỉ cần gắn sim vô là xài thôi, rất dễ dàng.",
-    },
-    {
-      name: "Yan Lin",
-      location: "Trung Quốc",
-      rating: 5,
-      avatar: "https://i.pravatar.cc/150?u=yan",
-      content:
-        "不用下载APP,不用注册账号,只要插入SIM卡就能上网。对不太懂技术的人来说非常友好,客服还可以用中文沟通,太贴心了!",
-    },
-    {
-      name: "Hồng Mây",
-      location: "Hồ Chí Minh",
-      rating: 5,
-      avatar: "https://i.pravatar.cc/150?u=may",
-      content:
-        "Dùng thích nên lần nào đi công tác mình cũng mua 😂 chất lượng dùng mạng bên TQ nhanh, vượt được hết tường lửa để truy cập zalo, fb thoải mái, giá rẻ mỗi tội không gọi được điện thôi.",
-    },
-    {
-      name: "Min-jun",
-      location: "Hàn Quốc",
-      rating: 5,
-      avatar: "https://i.pravatar.cc/150?u=minjun",
-      content:
-        "현지 SIM 카드인데 데이터 무제한 요금제가 있어서 너무 좋아요. 가격도 저렴하고 속도도 빠름! 카카오톡, 인스타, 유튜브 다 문제없이 사용 중이에요.",
-    },
-    {
-      name: "David",
-      location: "Hoa Kỳ",
-      rating: 5,
-      avatar: "https://i.pravatar.cc/150?u=david",
-      content:
-        "As a traveler from the U.S., I was worried about staying online during my trip. This SIM card solved everything. It was delivered to my hotel and activated in minutes.",
-    },
-    {
-      name: "Thùy Linh",
-      location: "Hà Nội",
-      rating: 5,
-      avatar: "https://i.pravatar.cc/150?u=linh",
-      content:
-        "Dịch vụ tuyệt vời ở Phuket, Thái Lan nhờ eSIM infigate. Kết nối mạnh ổn định, đủ để cả nhà xem phim, lướt web, sử dụng mạng xã hội một cách thoải mái.",
+  const dispatch = useAppDispatch();
+  // const testimonials = [
+  //   {
+  //     name: "Quang Huy",
+  //     location: "Nghệ An",
+  //     rating: 5,
+  //     avatar: "https://i.pravatar.cc/150?u=huy",
+  //     content:
+  //       "Trong suốt hành trình đi phượt bên Mỹ, Huy dò đường bằng Google Map phát trực tiếp từ sim data tốc độ cao mua sẵn ở Việt Nam tại shop infigate. Huy chỉ cần gắn sim vô là xài thôi, rất dễ dàng.",
+  //   },
+  //   {
+  //     name: "Yan Lin",
+  //     location: "Trung Quốc",
+  //     rating: 5,
+  //     avatar: "https://i.pravatar.cc/150?u=yan",
+  //     content:
+  //       "不用下载APP,不用注册账号,只要插入SIM卡就能上网。对不太懂技术的人来说非常友好,客服还可以用中文沟通,太贴心了!",
+  //   },
+  //   {
+  //     name: "Hồng Mây",
+  //     location: "Hồ Chí Minh",
+  //     rating: 5,
+  //     avatar: "https://i.pravatar.cc/150?u=may",
+  //     content:
+  //       "Dùng thích nên lần nào đi công tác mình cũng mua 😂 chất lượng dùng mạng bên TQ nhanh, vượt được hết tường lửa để truy cập zalo, fb thoải mái, giá rẻ mỗi tội không gọi được điện thôi.",
+  //   },
+  //   {
+  //     name: "Min-jun",
+  //     location: "Hàn Quốc",
+  //     rating: 5,
+  //     avatar: "https://i.pravatar.cc/150?u=minjun",
+  //     content:
+  //       "현지 SIM 카드인데 데이터 무제한 요금제가 있어서 너무 좋아요. 가격도 저렴하고 속도도 빠름! 카카오톡, 인스타, 유튜브 다 문제없이 사용 중이에요.",
+  //   },
+  //   {
+  //     name: "David",
+  //     location: "Hoa Kỳ",
+  //     rating: 5,
+  //     avatar: "https://i.pravatar.cc/150?u=david",
+  //     content:
+  //       "As a traveler from the U.S., I was worried about staying online during my trip. This SIM card solved everything. It was delivered to my hotel and activated in minutes.",
+  //   },
+  //   {
+  //     name: "Thùy Linh",
+  //     location: "Hà Nội",
+  //     rating: 5,
+  //     avatar: "https://i.pravatar.cc/150?u=linh",
+  //     content:
+  //       "Dịch vụ tuyệt vời ở Phuket, Thái Lan nhờ eSIM infigate. Kết nối mạnh ổn định, đủ để cả nhà xem phim, lướt web, sử dụng mạng xã hội một cách thoải mái.",
+  //   },
+  // ];
+
+  const { data: loadReviewData = [] } = useQuery<Review[]>({
+    queryKey: [DataCacheKey.REVIEWS],
+    queryFn: async (): Promise<Review[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await contentApi.LoadReview({
+          pageNumber: 0,
+          pageSize: 10,
+          isFeatured: true,
+        });
+        return res.data.reviews as Review[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
     },
-  ];
+    staleTime: staleTime,
+  });
 
   return (
     <section className="relative overflow-hidden bg-[#EE0434] py-10 md:py-20 lg:py-24">
@@ -73,15 +104,18 @@ const HomeTestimonial = () => {
             <div className="absolute inset-x-0 bottom-0 h-20 bg-gradient-to-t from-[#EE0434] to-transparent z-20"></div>
 
             <div className="flex flex-col space-y-6 animate-marquee-up hover:[animation-play-state:paused]">
-              {[...testimonials, ...testimonials].map((item, i) => (
+              {loadReviewData.map((item, i) => (
                 <TestimonialCard key={i} item={item} />
               ))}
             </div>
 
             <div className="flex flex-col space-y-6 animate-marquee-down hover:[animation-play-state:paused]">
-              {[...testimonials, ...testimonials].reverse().map((item, i) => (
-                <TestimonialCard key={i} item={item} />
-              ))}
+              {loadReviewData
+                .slice()
+                .reverse()
+                .map((item, i) => (
+                  <TestimonialCard key={i} item={item} />
+                ))}
             </div>
           </div>
         </div>
@@ -89,7 +123,7 @@ const HomeTestimonial = () => {
 
       <div className="lg:hidden w-screen overflow-hidden py-8 relative -ml-4">
         <div className="flex space-x-6 animate-scroll-x whitespace-nowrap px-4">
-          {[...testimonials, ...testimonials, ...testimonials].map(
+          {[...loadReviewData, ...loadReviewData, ...loadReviewData].map(
             (item, i) => (
               <div key={i} className="inline-block whitespace-normal align-top">
                 <TestimonialCard item={item} />

+ 320 - 22
EsimLao/esim-vite/src/pages/login/LoginView.tsx

@@ -1,41 +1,339 @@
-
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { authApi } from "../../apis/authApi";
+import {
+  startLoading,
+  startSmallLoading,
+  stopLoading,
+  stopSmallLoading,
+} from "../../features/loading/loadingSlice";
+import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
+import { useMutation } from "@tanstack/react-query";
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useEffect, useRef } from "react";
+import { accountLogin } from "../../features/account/accuntSlice";
 
 const LoginView: React.FC = () => {
   const navigate = useNavigate();
-  const [email, setEmail] = useState('');
+  const loading = useAppSelector((state) => state.loading);
+  const dispatch = useAppDispatch();
+  const [email, setEmail] = useState("");
+  const [step, setStep] = useState<"email" | "otp">("email");
+
+  const [otp, setOtp] = useState("");
+  const [timer, setTimer] = useState(60);
+  const inputRef = useRef<HTMLInputElement>(null);
+  const [errorMessage, setErrorMessage] = useState("");
+
+  useEffect(() => {
+    // Focus input on mount
+    inputRef.current?.focus();
+  }, []);
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const val = e.target.value.replace(/[^0-9]/g, "");
+    if (val.length <= 6) {
+      setOtp(val);
+    }
+  };
+
+  const handleVerify = async (e: React.FormEvent) => {
+    e.preventDefault();
+    // Case 1: Timer expired -> Button acts as RESEND
+    if (timer === 0) {
+      handleResend();
+      return;
+    }
+    if (otp.length !== 6) return;
+    setErrorMessage("");
+    verifyOtpMutation.mutate();
+  };
+
+  const handleResend = () => {
+    setTimer(60);
+    setOtp("");
+    // Re-focus input after a slight delay to allow render update
+    setTimeout(() => inputRef.current?.focus(), 50);
+  };
+
+  const getOtpMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startSmallLoading());
+      const res = await authApi.requestOtp({ email });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopSmallLoading());
+      console.log("Get otp response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Get otp successful");
+        // show otp step
+        // Countdown timer
+        setStep("otp");
+
+        const interval = setInterval(() => {
+          setTimer((prev) => (prev > 0 ? prev - 1 : 0));
+        }, 1000);
+        return () => clearInterval(interval);
+      } else {
+        console.error("Get otp failed, no token received");
+        setErrorMessage(
+          data?.message || "Failed to request OTP. Please try again."
+        );
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopSmallLoading());
+      console.error("Get otp error:", error.response.data);
+    },
+  });
+
+  const verifyOtpMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startSmallLoading());
+      const res = await authApi.verifyOtp({ email, otp });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopSmallLoading());
+      console.log("Verify otp response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Verify otp successful");
+        // save token to local storage
+        dispatch(accountLogin(data.data));
+        // redirect to home
+        dispatch(startLoading({ message: "Logging in..." }));
+        navigate("/");
+      } else {
+        console.error("Verify otp failed, no token received");
+        setErrorMessage(
+          data?.message || "Failed to verify OTP. Please try again."
+        );
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopSmallLoading());
+      console.error("Verify otp error:", error.response.data);
+    },
+  });
+
+  const handleGetOtp = (e: React.FormEvent) => {
+    e.preventDefault();
+    setErrorMessage("");
+    getOtpMutation.mutate();
+  };
 
   return (
     <div className="min-h-screen bg-white flex flex-col lg:flex-row overflow-hidden">
       <div className="w-full lg:w-1/2 flex flex-col justify-center items-center p-6 md:p-12 lg:p-20 relative z-10 bg-white">
-        <button onClick={() => navigate(-1)} className="absolute top-6 left-6 lg:hidden p-2 text-slate-400 hover:text-[#EE0434] transition-colors">
-          <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M15 19l-7-7 7-7" /></svg>
+        <button
+          onClick={() => navigate(-1)}
+          className="absolute top-6 left-6 lg:hidden p-2 text-slate-400 hover:text-[#EE0434] transition-colors"
+        >
+          <svg
+            className="w-6 h-6"
+            fill="none"
+            stroke="currentColor"
+            viewBox="0 0 24 24"
+          >
+            <path
+              strokeLinecap="round"
+              strokeLinejoin="round"
+              strokeWidth={2.5}
+              d="M15 19l-7-7 7-7"
+            />
+          </svg>
         </button>
         <div className="w-full max-w-[420px] space-y-10">
           <div className="space-y-4 text-center lg:text-left">
             <div className="lg:hidden flex justify-center mb-8">
-               <div className="w-20 h-20 rounded-[24px] bg-gradient-to-br from-[#EE0434] to-[#80001a] flex items-center justify-center shadow-lg">
-                  <svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"/></svg>
-               </div>
+              <div className="w-20 h-20 rounded-[24px] bg-gradient-to-br from-[#EE0434] to-[#80001a] flex items-center justify-center shadow-lg">
+                <svg
+                  className="w-10 h-10 text-white"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
+                    strokeWidth={2}
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                  />
+                </svg>
+              </div>
             </div>
-            <h1 className="text-3xl md:text-4xl lg:text-5xl font-black text-slate-900 tracking-tight">Welcome Back!</h1>
-            <p className="text-slate-400 font-medium text-base md:text-lg">Stay connected everywhere with InfiGate.</p>
+            <h1 className="text-3xl md:text-4xl lg:text-5xl font-black text-slate-900 tracking-tight">
+              Welcome Back!
+            </h1>
+            <p className="text-slate-400 font-medium text-base md:text-lg">
+              Stay connected everywhere with InfiGate.
+            </p>
           </div>
-          <form className="space-y-8" onSubmit={(e) => e.preventDefault()}>
-            <div className="space-y-3">
-              <label className="block text-slate-500 font-black text-[10px] uppercase tracking-[0.2em] pl-1">Email Address</label>
-              <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Enter your email" className="w-full bg-slate-50 border-2 border-transparent focus:border-[#EE0434]/20 rounded-2xl py-4 md:py-5 px-6 focus:outline-none focus:bg-white transition-all text-slate-700 font-semibold" />
-            </div>
-            <button type="submit" className="w-full bg-[#EE0434] text-white py-4 md:py-5 rounded-2xl font-black text-xl md:text-2xl shadow-lg hover:scale-[1.01] active:scale-[0.98] transition-all">Login</button>
-          </form>
+          {step === "email" && (
+            <form className="space-y-8" onSubmit={handleGetOtp}>
+              <div className="space-y-3">
+                <label className="block text-slate-500 font-black text-[10px] uppercase tracking-[0.2em] pl-1">
+                  Email Address
+                </label>
+                <input
+                  type="email"
+                  value={email}
+                  onChange={(e) => setEmail(e.target.value)}
+                  placeholder="Enter your email"
+                  className="w-full bg-slate-50 border-2 border-transparent focus:border-[#EE0434]/20 rounded-2xl py-4 md:py-5 px-6 focus:outline-none focus:bg-white transition-all text-slate-700 font-semibold"
+                />
+              </div>
+              <div>
+                {errorMessage && (
+                  <p className="text-red-500 text-sm font-medium text-base md:text-lg text-center lg:text-left mb-4">
+                    {errorMessage}
+                  </p>
+                )}
+              </div>
+              <button
+                type="submit"
+                disabled={loading.isSmallLoading}
+                className={`w-full py-4 md:py-5 rounded-2xl font-black text-xl md:text-2xl shadow-lg transition-all flex items-center justify-center space-x-3 ${
+                  !loading.isSmallLoading
+                    ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
+                    : "bg-slate-100 text-slate-300 cursor-not-allowed"
+                }`}
+              >
+                {loading.isSmallLoading && (
+                  <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
+                )}
+                <span>Login</span>
+              </button>
+            </form>
+          )}
+          {step === "otp" && (
+            <>
+              <button
+                onClick={() => setStep("email")}
+                className="absolute top-6 left-6 p-2 text-slate-400 hover:text-[#EE0434] transition-colors"
+              >
+                <svg
+                  className="w-6 h-6"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2.5}
+                    d="M15 19l-7-7 7-7"
+                  />
+                </svg>
+              </button>
+              <div className="w-full max-w-[420px] space-y-10">
+                <div className="space-y-4 text-center lg:text-left">
+                  <h1 className="text-3xl md:text-4xl lg:text-5xl font-black text-slate-900 tracking-tight">
+                    Check your email
+                  </h1>
+                  <p className="text-slate-400 font-medium text-base md:text-lg">
+                    We've sent a 6-digit verification code to <br />
+                    <span className="text-slate-800 font-bold">{email}</span>
+                  </p>
+                </div>
+
+                <form className="space-y-8" onSubmit={handleVerify}>
+                  <div className="space-y-4">
+                    <label className="block text-slate-500 font-black text-[10px] uppercase tracking-[0.2em] pl-1">
+                      Verification Code
+                    </label>
+
+                    <div
+                      className="relative h-16 md:h-20 w-full"
+                      onClick={() => timer > 0 && inputRef.current?.focus()}
+                    >
+                      {/* Visual Boxes */}
+                      <div
+                        className={`absolute inset-0 flex justify-between gap-2 md:gap-4 pointer-events-none ${
+                          timer === 0 ? "opacity-50" : "opacity-100"
+                        }`}
+                      >
+                        {[...Array(6)].map((_, idx) => (
+                          <div
+                            key={idx}
+                            className={`flex-1 rounded-xl md:rounded-2xl border-2 flex items-center justify-center text-2xl md:text-3xl font-black transition-all ${
+                              otp.length === idx && timer > 0
+                                ? "border-[#EE0434] bg-white shadow-lg ring-4 ring-red-50"
+                                : otp.length > idx
+                                ? "border-[#EE0434] bg-red-50 text-[#EE0434]"
+                                : "border-slate-100 bg-slate-50 text-slate-300"
+                            }`}
+                          >
+                            {otp[idx] || ""}
+                          </div>
+                        ))}
+                      </div>
+
+                      {/* Hidden Input */}
+                      <input
+                        ref={inputRef}
+                        type="text"
+                        inputMode="numeric"
+                        autoComplete="one-time-code"
+                        value={otp}
+                        onChange={handleChange}
+                        disabled={timer === 0}
+                        className="absolute inset-0 w-full h-full opacity-0 cursor-text disabled:cursor-not-allowed"
+                        maxLength={6}
+                      />
+                    </div>
+                  </div>
+                  <div>
+                    {errorMessage && (
+                      <p className="text-red-500 text-sm font-medium text-base md:text-lg text-center lg:text-left mb-4">
+                        {errorMessage}
+                      </p>
+                    )}
+                  </div>
+                  <button
+                    type="submit"
+                    // Disabled if: (Timer active AND (otp not full OR verifying))
+                    disabled={
+                      (timer > 0 && otp.length !== 6) || loading.isSmallLoading
+                    }
+                    className={`w-full py-4 md:py-5 rounded-2xl font-black text-xl md:text-2xl shadow-lg transition-all flex items-center justify-center space-x-3 ${
+                      timer === 0 ||
+                      (otp.length === 6 && !loading.isSmallLoading)
+                        ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
+                        : "bg-slate-100 text-slate-300 cursor-not-allowed"
+                    }`}
+                  >
+                    {loading.isSmallLoading && (
+                      <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
+                    )}
+                    <span>{timer === 0 ? "Resend OTP" : "Verify"}</span>
+                  </button>
+                </form>
+
+                <div className="text-center">
+                  {timer > 0 ? (
+                    <p className="text-slate-500 font-medium">
+                      Code expires in{" "}
+                      <span className="text-slate-900 font-bold">{timer}s</span>
+                    </p>
+                  ) : (
+                    <p className="text-[#EE0434] font-medium animate-pulse">
+                      Code expired. Please request a new one.
+                    </p>
+                  )}
+                </div>
+              </div>{" "}
+            </>
+          )}
         </div>
       </div>
       <div className="hidden lg:flex w-1/2 bg-gradient-to-br from-[#EE0434] to-[#80001a] items-center justify-center relative overflow-hidden">
-         <div className="relative z-10 text-white text-center space-y-4">
-             <h2 className="text-5xl font-black">Fast. Simple. Red.</h2>
-             <p className="text-xl opacity-80">Global connectivity for the modern traveler.</p>
-         </div>
+        <div className="relative z-10 text-white text-center space-y-4">
+          <h2 className="text-5xl font-black">Fast. Simple. Red.</h2>
+          <p className="text-xl opacity-80">
+            Global connectivity for the modern traveler.
+          </p>
+        </div>
       </div>
     </div>
   );

+ 46 - 11
EsimLao/esim-vite/src/pages/news/NewsDetailView.tsx

@@ -1,11 +1,45 @@
-import React from "react";
+import React, { useState, useEffect, useCallback } from "react";
 import { useLocation, useNavigate, Link } from "react-router-dom";
 import { NewsArticle } from "../../services/types";
+import { useMutation } from "@tanstack/react-query";
+import { useAppDispatch } from "../../hooks/useRedux";
+import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
+import { articleApi } from "../../apis/articleApi";
+import { Article } from "../../services/article/types";
 
 const ArticleDetailView: React.FC = () => {
   const location = useLocation();
   const navigate = useNavigate();
-  const article = location.state?.article as NewsArticle;
+  const dispatch = useAppDispatch();
+  const article = location.state?.article as Article;
+
+  // load article detail
+  const loadArticleDetailMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await articleApi.LoadArticleDetail({ articleId: article.id });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopLoading());
+      console.log("Get article detail response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Get article detail successful");
+      } else {
+        console.error("Get article detail failed, no token received");
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopLoading());
+      console.error("Get article detail error:", error.response.data);
+    },
+  });
+
+  useEffect(() => {
+    if (article && article.id) {
+      loadArticleDetailMutation.mutate();
+    }
+  }, [article]);
 
   if (!article) {
     return (
@@ -81,30 +115,31 @@ const ArticleDetailView: React.FC = () => {
                 d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
               />
             </svg>
-            <span className="text-xs md:text-sm font-bold">{article.date}</span>
+            <span className="text-xs md:text-sm font-bold">
+              {article.publishedDate}
+            </span>
           </div>
         </header>
 
         <div className="mb-8 md:mb-12">
           <div className="aspect-[21/9] rounded-[24px] md:rounded-[40px] overflow-hidden shadow-2xl">
             <img
-              src={article.image}
+              src={article.coverImageUrl}
               alt={article.title}
               className="w-full h-full object-cover"
             />
           </div>
         </div>
 
-        <div className="max-w-4xl mx-auto">
+        <div className="mx-auto">
           <article className="prose prose-lg prose-slate max-w-none text-slate-800">
             <h2 className="text-lg md:text-xl font-black mb-4 text-slate-900 leading-snug">
-              {article.title}: Chi tiết – Dễ hiểu – Thành công 100%
+              {article.title}
             </h2>
-            <p className="text-base md:text-lg leading-relaxed mb-4 font-medium text-slate-600">
-              Trong những năm gần đây, eSIM du lịch trở thành lựa chọn tối ưu
-              cho người đi công tác và du lịch nước ngoài nhờ tiện lợi, kích
-              hoạt nhanh, giá hợp lý và hoàn toàn không cần tháo SIM vật lý.
-            </p>
+            {/* <p className="text-base md:text-lg leading-relaxed mb-4 font-medium text-slate-600"> */}
+            {/* show html  */}
+            <div dangerouslySetInnerHTML={{ __html: article.content }} />
+            {/* </p> */}
           </article>
 
           <div className="mt-16 pt-8 border-t border-slate-100 flex flex-col md:flex-row justify-between items-center gap-6">

+ 36 - 54
EsimLao/esim-vite/src/pages/news/NewsView.tsx

@@ -1,61 +1,41 @@
 import React from "react";
 import { useNavigate, Link } from "react-router-dom";
 import { NewsArticle } from "../../services/types";
+import { DataCacheKey, staleTime } from "../../global/constants";
+import { useQuery } from "@tanstack/react-query";
+import { Article, Category } from "@/src/services/article/types";
+import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
+import { useAppDispatch } from "../../hooks/useRedux";
+import { articleApi } from "../../apis/articleApi";
 
 const NewsView: React.FC = () => {
   const navigate = useNavigate();
-  const articles: NewsArticle[] = [
-    {
-      id: "1",
-      title:
-        "Siêu Sale Mùa Lễ Hội Cùng InfiGate – Giảm Đến 35% Khi Mua eSIM Du Lịch Quốc Tế",
-      date: "01/12/2025",
-      excerpt:
-        "Siêu Sale Mùa Lễ Hội Cùng InfiGate – Giảm Đến 35% Thời gian áp dụng ưu đãi: 01/12 – 31/12/2025",
-      image:
-        "https://images.unsplash.com/photo-1549463599-231a5401977e?q=80&w=800&auto=format&fit=crop",
-    },
-    {
-      id: "2",
-      title:
-        "Du lịch châu Á tháng 12: Mùa lễ hội rực rỡ và những trải nghiệm không thể bỏ lỡ – Đừng quên chuẩn bị SIM du lịch trước khi khởi hành!",
-      date: "28/11/2025",
-      excerpt:
-        "Tháng 12 là thời điểm lý tưởng để khám phá châu Á: từ mùa tuyết trắng Nhật – Hàn, biển ấm Đông...",
-      image:
-        "https://images.unsplash.com/photo-1493976040374-85c8e12f0c0e?q=80&w=800&auto=format&fit=crop",
-    },
-    {
-      id: "3",
-      title:
-        "InfiGate ra mắt bộ đôi eSIM & SIM du lịch – Mở rộng lựa chọn, tối ưu kết nối toàn cầu",
-      date: "27/11/2025",
-      excerpt:
-        "Khi nhu cầu du lịch bùng nổ mạnh mẽ, Internet trở thành “vũ khí tối thượng” cho mọi chuyến đi: dẫn...",
-      image:
-        "https://images.unsplash.com/photo-1526772662000-3f88f10c053b?q=80&w=800&auto=format&fit=crop",
-    },
-    {
-      id: "4",
-      title: "Hướng dẫn cách cài đặt eSIM du lịch trên Samsung",
-      date: "14/11/2025",
-      excerpt:
-        "Hướng dẫn cài đặt eSIM du lịch trên Samsung nhanh và đơn giản. Áp dụng cho Galaxy S, A, Z. Gợi ý eSIM...",
-      image:
-        "https://images.unsplash.com/photo-1610945265064-0e34e5519bbf?q=80&w=800&auto=format&fit=crop",
-    },
-    {
-      id: "5",
-      title: "Hướng dẫn cách cài đặt eSIM du lịch trên iPhone",
-      date: "14/11/2025",
-      excerpt:
-        "Hướng dẫn chi tiết cách kích hoạt eSIM trên các dòng iPhone từ XS trở lên. Giải pháp kết nối tối ưu...",
-      image:
-        "https://images.unsplash.com/photo-1510557880182-3d4d3cba35a5?q=80&w=1000&auto=format&fit=crop",
+  const dispatch = useAppDispatch();
+
+  const { data: loadArticleData = [] } = useQuery<Article[]>({
+    queryKey: [DataCacheKey.ARTICLES],
+    queryFn: async (): Promise<Article[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await articleApi.LoadArticle({
+          pageNumber: 0,
+          pageSize: 10,
+          categoryId: null,
+          isFeatured: true,
+        });
+        console.log("Loaded articles: ", res.data.articles);
+        return res.data.articles as Article[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
     },
-  ];
+    staleTime: staleTime,
+  });
 
-  const handleArticleClick = (article: NewsArticle) => {
+  const handleArticleClick = (article: Article) => {
     navigate(`/news/${article.id}`, { state: { article } });
     window.scrollTo({ top: 0, behavior: "smooth" });
   };
@@ -86,7 +66,7 @@ const NewsView: React.FC = () => {
 
       <div className="max-w-7xl mx-auto px-4 py-12">
         <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
-          {articles.map((article) => (
+          {loadArticleData.map((article) => (
             <div
               key={article.id}
               onClick={() => handleArticleClick(article)}
@@ -94,7 +74,7 @@ const NewsView: React.FC = () => {
             >
               <div className="relative aspect-[4/3] overflow-hidden">
                 <img
-                  src={article.image}
+                  src={article.thumbnailUrl}
                   alt={article.title}
                   className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
                 />
@@ -114,13 +94,15 @@ const NewsView: React.FC = () => {
                       d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
                     />
                   </svg>
-                  <span className="text-xs font-bold">{article.date}</span>
+                  <span className="text-xs font-bold">
+                    {article.publishedDate}
+                  </span>
                 </div>
                 <h3 className="text-[16px] md:text-[18px] font-black text-slate-800 leading-tight group-hover:text-[#EE0434] transition-colors line-clamp-3">
                   {article.title}
                 </h3>
                 <p className="text-xs md:text-sm text-slate-500 font-medium line-clamp-3 leading-relaxed">
-                  {article.excerpt}
+                  {article.summary}
                 </p>
               </div>
             </div>

+ 49 - 0
EsimLao/esim-vite/src/services/article/types.ts

@@ -0,0 +1,49 @@
+export interface Category {
+  id: number;
+  categoryName: string;
+  categorySlug: string;
+  description: string;
+  iconUrl: string;
+  parentId: number | null;
+  displayOrder: number;
+}
+
+export interface Pagination {
+  pageNumber: number;
+  pageSize: number;
+  totalCount: number;
+  totalPages: number;
+}
+
+export interface LoadCategoryResponse {
+  categories: Category[];
+  pagination: Pagination;
+}
+
+export interface Article {
+  categoryId: number;
+  id: number;
+  isFeatured: boolean;
+  isPinned: boolean;
+  publishedDate: string;
+  slug: string;
+  thumbnailUrl: string;
+  viewCount: number;
+  summary: string;
+  title: string;
+
+  content: string;
+  coverImageUrl: string;
+  metaDescription: string;
+  metaKeywords: string;
+  createdDate: string;
+}
+
+export interface LoadArticleResponse {
+  articles: Article[];
+  pagination: Pagination;
+}
+
+export interface LoadArticleDetailResponse {
+  article: Article;
+}

+ 9 - 0
EsimLao/esim-vite/src/services/auth/types.ts

@@ -0,0 +1,9 @@
+export interface AccountInfo {
+  userId: number;
+  email: string;
+  fullName: string;
+  avatarUrl: string;
+  accessToken: string;
+  refreshToken: string;
+  expiresAt: string;
+}

+ 62 - 0
EsimLao/esim-vite/src/services/content/types.ts

@@ -0,0 +1,62 @@
+import { Pagination } from "../article/types";
+
+export interface Banner {
+  id: number;
+  title: string;
+  subtitle: string;
+  imageUrl: string;
+  imageMobileUrl: string;
+  linkUrl: string;
+  linkTarget: string;
+  position: string;
+  displayOrder: number;
+}
+
+export interface LoadBannerResponse {
+  banners: Banner[];
+  pagination: Pagination;
+}
+
+export interface Review {
+  id: number;
+  customerName: string;
+  avatarUrl: string;
+  rating: number;
+  reviewContent: string;
+  destination: string;
+  isFeatured: boolean;
+  createdDate: string;
+}
+
+export interface LoadReviewResponse {
+  reviews: Review[];
+  pagination: Pagination;
+}
+
+export interface FaqCategory {
+  id: number;
+  categoryName: string;
+  categorySlug: string;
+  description: string;
+  iconUrl: string;
+  displayOrder: number;
+}
+
+export interface Faq {
+  id: number;
+  question: string;
+  answer: string;
+  categoryId: number;
+  viewCount: number;
+  isFeatured: boolean;
+}
+
+export interface LoadFaqCategoryResponse {
+  categories: FaqCategory[];
+  pagination: Pagination;
+}
+
+export interface LoadFaqResponse {
+  faqs: Faq[];
+  pagination: Pagination;
+}

+ 14 - 12
EsimLao/esim-vite/src/services/product/type.ts

@@ -1,13 +1,15 @@
 export interface AreaData {
-    id: number;
-    areaCode: string;
-    areaName1: string;
-    areaName2: string;
-    status: number;
-    imgUrl: string | null;
-    iconUrl: string | null;
-    isPopular: number;
-    isCountry: number;
-    minDisplayPrice: number;
-    minSellPrice: number;
-}
+  id: number;
+  areaCode: string;
+  areaName1: string;
+  areaName2: string;
+  status: number;
+  imgUrl: string | null;
+  iconUrl: string | null;
+  isPopular: number;
+  isCountry: number;
+  minDisplayPrice: number;
+  minSellPrice: number;
+  promotionPercent: number;
+  curency: string;
+}

+ 9 - 8
EsimLao/esim-vite/vite.config.ts

@@ -1,18 +1,19 @@
-import path from 'path';
-import { defineConfig, loadEnv } from 'vite';
-import react from '@vitejs/plugin-react';
+import path from "path";
+import { defineConfig, loadEnv } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
 
 export default defineConfig(({ mode }) => {
-  const env = loadEnv(mode, './', '');
+  const env = loadEnv(mode, "./", "");
   return {
     server: {
       port: 3000,
-      host: '0.0.0.0',
+      host: "0.0.0.0",
     },
-    plugins: [react()],
+    plugins: [react(), tailwindcss()],
     define: {
-      'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
-      'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
+      "process.env.API_KEY": JSON.stringify(env.GEMINI_API_KEY),
+      "process.env.GEMINI_API_KEY": JSON.stringify(env.GEMINI_API_KEY),
     },
     // resolve: {
     //   alias: {

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.