ソースを参照

feat: Implement internationalization in LoginView, ProductDetailView, and SupportView

- Added translation support using react-i18next in LoginView for welcome messages and form labels.
- Updated ProductDetailView to include translations for product details and navigation links.
- Enhanced SupportView with translations for support topics and navigation.
- Introduced new components: ProductInfoModal for displaying detailed product information and PackageOverview for summarizing package details.
- Refactored Package interface to include additional fields for better product representation.
- Integrated related area suggestions in ProductDetailView to enhance user experience.
trunghieubui 3 週間 前
コミット
29298aa28e

+ 4 - 4
EsimLao/esim-vite/src/App.tsx

@@ -57,8 +57,8 @@ const App: React.FC = () => {
             <Routes>
               <Route path="/" element={<HomeView />} />
               <Route path="/buy-sim" element={<BuySimView />} />
-              <Route path="/product/:country" element={<ProductDetailView />} />
-              <Route path="/product" element={<ProductDetailView />} />
+              <Route path="/product/:id" element={<ProductDetailView />} />
+              {/* <Route path="/product" element={<ProductDetailView />} /> */}
               <Route path="/checkout" element={<CheckoutView />} />
               <Route path="/support" element={<SupportView />} />
               <Route path="/news" element={<NewsView />} />
@@ -70,10 +70,10 @@ const App: React.FC = () => {
                 path="/order-history-detail/:id"
                 element={<OrderDetailView />}
               />
-              <Route
+              {/* <Route
                 path="/order-history-detail"
                 element={<OrderDetailView />}
-              />
+              /> */}
               {/* Fallback */}
               <Route path="*" element={<Navigate to="/" replace />} />
             </Routes>

+ 6 - 2
EsimLao/esim-vite/src/apis/productApi.ts

@@ -17,8 +17,8 @@ class ProductApi extends BaseApi {
     });
   }
 
-  async loadPackage({ areaId, isUnlimited = "-1", isDaily = "-1" }) {
-    return this.productPost("/list_packg", { areaId, isUnlimited, isDaily });
+  async loadPackage({ areaId, dataType = "-1" }) {
+    return this.productPost("/list_packg", { areaId, dataType });
   }
 
   async checkout({ packgId, quantity }) {
@@ -63,6 +63,10 @@ class ProductApi extends BaseApi {
   async checkDataUsage({ iccid }) {
     return this.productPost("/check_data_usage", { iccid });
   }
+
+  async loadRelatedArea({ areaId }) {
+    return this.productPost("/related_area", { areaId });
+  }
 }
 
 export const productApi = new ProductApi();

+ 5 - 5
EsimLao/esim-vite/src/components/Header.tsx

@@ -43,10 +43,10 @@ const Header: React.FC = () => {
   const account = localStorage.getItem("accountInfo");
 
   const guideItems = [
-    { label: "What is eSIM", path: "/support" },
-    { label: "Installation instructions", path: "/support" },
-    { label: "Support", path: "/support" },
-    { label: "Order Tracking Search", path: "/support" },
+    { label: t("whatEsim"), path: "/support" },
+    { label: t("installationInstructions"), path: "/support" },
+    { label: t("supportSupport"), path: "/support" },
+    { label: t("orderSearch"), path: "/support" },
   ];
   const dropdownRef = useRef<HTMLDivElement>(null);
   const [areasList, setAreasList] = useState<Area[]>([]);
@@ -238,7 +238,7 @@ const Header: React.FC = () => {
                     handleSearch(e.target.value);
                   }}
                   onFocus={() => setIsSearchDropdownOpen(true)}
-                  placeholder="Search country..."
+                  placeholder={t("searchCountry")}
                   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

+ 5 - 1
EsimLao/esim-vite/src/components/ProductCard.tsx

@@ -3,6 +3,7 @@ import React from "react";
 import { SimProduct } from "../services/types";
 import { Area } from "../services/product/type";
 import { useTranslation } from "react-i18next";
+import { formatNumber } from "../logic/loigicUtils";
 
 const ProductCard: React.FC<{
   p: Area;
@@ -28,10 +29,13 @@ const ProductCard: React.FC<{
         <h3 className="text-slate-900 font-black text-sm md:text-lg mb-0.5 group-hover:text-[#EE0434] transition-colors truncate px-1">
           {p.areaName1}
         </h3>
+        <p className="text-xs text-slate-400 line-through mb-1">
+          {formatNumber(p.minDisplayPrice)} {p.curency}
+        </p>
         <p className="text-[#EE0434] font-bold text-xs md:text-lg">
           {t("from")}:{" "}
           <span className="font-black">
-            {p.minDisplayPrice} {p.curency}
+            {formatNumber(p.minSellPrice)} {p.curency}
           </span>
         </p>
       </div>

+ 381 - 0
EsimLao/esim-vite/src/components/ProductInfoModal.tsx

@@ -0,0 +1,381 @@
+import React, { useState } from "react";
+import { Area, Package } from "../services/product/type";
+import { useTranslation } from "react-i18next";
+
+interface ProductInfoModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  packageInfo: Package | null;
+}
+
+const ProductInfoModal: React.FC<ProductInfoModalProps> = ({
+  isOpen,
+  onClose,
+  packageInfo,
+}) => {
+  const { t } = useTranslation();
+  const [activeTab, setActiveTab] = useState<"specs" | "notes">("specs");
+
+  if (!isOpen || !packageInfo) return null;
+
+  const specs = [
+    {
+      label: t("coverageArea"),
+      value: packageInfo?.coverageArea,
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("networkProvider"),
+      value: packageInfo?.carriers,
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("infoRegistration") + " (eKYC)",
+      value: packageInfo?.ekycRequired ? t("required") : t("notRequired"),
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("activationMethod"),
+      value:
+        packageInfo?.activationMethod === 1
+          ? t("usageTimeCalculatedFromSignal")
+          : t("usageTimeCalculatedFromPurchase"),
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 14.5V12m0 0V8.832c0-1.12 1.682-1.43 2.182-1.832l.75-1.05a.5.5 0 00-.864-.53l-1.07 1.635M12 21a9 9 0 110-18 9 9 0 010 18z"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("deliveryMethod"),
+      value:
+        packageInfo?.deliveryMethod === 0
+          ? t(
+              "youWillReceiveAnEmailImmediatelyAfterPaymentWithEsimWaitForShippingAndReceiveYourPhysicalSim"
+            )
+          : t("physicalSimWillBeShippedToYourAddressWithin3-5BusinessDays"),
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 10-4 0 2 2 0 004 0zm10 0a2 2 0 10-4 0 2 2 0 004 0z"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("phoneNumber"),
+      value: packageInfo?.phoneNumber != 0 ? packageInfo.phoneNumber : "No",
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("wifiHotspot"),
+      value: packageInfo?.wifiSharing === 1 ? t("yesSupported") : t("no"),
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("networkTechnology"),
+      value: packageInfo?.networkTechnology,
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("packageStartTime"),
+      value:
+        packageInfo?.activationMethod === 1
+          ? t("usageTimeCalculatedFromSignal")
+          : t("usageTimeCalculatedFromPurchase"),
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("dataResetTime"),
+      value:
+        packageInfo?.dataResetTime === 0
+          ? t("timeResetInfo")
+          : t("timeResetInfo"),
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("unlimitedPolicy"),
+      value: packageInfo?.unlimitedPolicy,
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M13 10V3L4 14h7v7l9-11h-7z"
+          />
+        </svg>
+      ),
+    },
+    {
+      label: t("appSupport"),
+      value: packageInfo?.appSupport,
+      icon: (
+        <svg
+          className="w-5 h-5 color-EE0434"
+          fill="none"
+          stroke="currentColor"
+          viewBox="0 0 24 24"
+        >
+          <path
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            strokeWidth={2}
+            d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"
+          />
+        </svg>
+      ),
+    },
+  ];
+
+  return (
+    <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 animate-in fade-in duration-200">
+      <div
+        className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"
+        onClick={onClose}
+      ></div>
+
+      <div className="relative bg-white rounded-[32px] w-full max-w-2xl max-h-[85vh] shadow-2xl flex flex-col overflow-hidden">
+        {/* Header */}
+        <div className="p-6 pb-2 border-b border-slate-100 flex justify-between items-center bg-white z-10">
+          <h2 className="text-xl md:text-2xl font-black text-slate-900">
+            {t("productInformation")}
+          </h2>
+          <button
+            onClick={onClose}
+            className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-full transition-all"
+          >
+            <svg
+              className="w-6 h-6"
+              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>
+
+        {/* Tabs */}
+        <div className="flex px-6 border-b border-slate-100 bg-white z-10">
+          <button
+            onClick={() => setActiveTab("specs")}
+            className={`flex-1 py-4 text-sm md:text-base font-bold text-center border-b-2 transition-colors ${
+              activeTab === "specs"
+                ? "border-[#EE0434] text-[#EE0434]"
+                : "border-transparent text-slate-400 hover:text-slate-600"
+            }`}
+          >
+            {t("specifications")}
+          </button>
+          <button
+            onClick={() => setActiveTab("notes")}
+            className={`flex-1 py-4 text-sm md:text-base font-bold text-center border-b-2 transition-colors ${
+              activeTab === "notes"
+                ? "border-[#EE0434] text-[#EE0434]"
+                : "border-transparent text-slate-400 hover:text-slate-600"
+            }`}
+          >
+            {t("notes")}
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="flex-1 overflow-y-auto p-6 md:p-8 custom-scrollbar bg-white">
+          {activeTab === "specs" ? (
+            <div className="space-y-6">
+              {specs.map((spec, idx) => (
+                <div key={idx} className="flex items-start space-x-4">
+                  <div className="w-6 h-6 text-[#0091ff] shrink-0 mt-0.5">
+                    {spec.icon}
+                  </div>
+                  <div className="flex-1">
+                    <p className="font-bold text-slate-600 text-sm mb-1">
+                      {spec.label}:
+                    </p>
+                    <p className="text-slate-800 font-medium text-sm md:text-base leading-relaxed">
+                      {spec.value}
+                    </p>
+                  </div>
+                </div>
+              ))}
+            </div>
+          ) : (
+            <div className="space-y-4 text-slate-600">
+              <p className="font-medium leading-relaxed">
+                <span className="font-bold text-[16px] color-EE0434">1.</span>{" "}
+                {t("note1")}
+              </p>
+              <p className="font-medium leading-relaxed">
+                <span className="font-bold text-[16px] color-EE0434">2.</span>{" "}
+                {t("note2")}
+              </p>
+              <p className="font-medium leading-relaxed">
+                <span className="font-bold text-[16px] color-EE0434">3.</span>{" "}
+                {t("note3")}
+              </p>
+              <p className="font-medium leading-relaxed">
+                <span className="font-bold text-[16px] color-EE0434">4.</span>{" "}
+                {t("note4")}
+              </p>
+              <p className="font-medium leading-relaxed">
+                <span className="font-bold text-[16px] color-EE0434">5.</span>{" "}
+                {t("note5")}
+              </p>
+              <p className="font-medium leading-relaxed">
+                <span className="font-bold text-[16px] color-EE0434">6.</span>{" "}
+                {t("note6")}
+              </p>
+            </div>
+          )}
+        </div>
+
+        {/* Bottom Fade */}
+        <div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white to-transparent pointer-events-none"></div>
+      </div>
+    </div>
+  );
+};
+
+export default ProductInfoModal;

+ 69 - 5
EsimLao/esim-vite/src/i18n/locales/en.json

@@ -10,12 +10,12 @@
   "orderSearch": "Order Tracking Search",
   "seeMore": "See more",
   "whatUs": "What our customers say about us",
-  "whyGetgo": "Why choose Getgo",
+  "chooseGetgo": "Why choose Getgo",
   "fastCost": "Fast – Convenient – Reasonable Cost",
   "globalAnywhere": "Global connection, Stable Anywhere",
   "customerSupport": "24/7 Customer Support",
   "customerPolicy": "Customer-first Policy",
-  "getgoWith": "Getgo has partnered with",
+  "getgoWith": "has partnered with",
   "buyExplore": "Buy a SIM easily, ready to explore",
   "compatibleDevice": "Compatible Device",
   "pickPlan": "Pick Your Plan",
@@ -25,8 +25,8 @@
   "returnGuide": "Return and Refund Guide",
   "chooseTo": "Choose country you're going to",
   "doesEsim?": "Does my phone support eSIM?",
-  "ordersOrders": "Orders",
-  "logoutLogout": "Logout",
+  "orders": "Orders",
+  "logout": "Logout",
   "viettechLimited": "VIETTECH Global Development Company Limited",
   "taxCode": "Tax Code: 0901210362",
   "address": "218 Dao Dua 1, Vinhomes Ocean Park 2 Urban Area, Nghia Tru Commune, Hung Yen Province, Vietnam",
@@ -142,5 +142,69 @@
   "realTimeWebSearchPoweredByGoogleGrounding": "Real-time web search powered by Google Grounding",
   "scouringTheWebForRealTimeInformation": "Scouring the web for real-time information...",
   "news": "News",
-  "contact": "Contact"
+  "contact": "Contact",
+  "home": "Home",
+  "productInformation": "Product Information",
+  "networkProvider": "Network Provider",
+  "infoRegistration": "Info Registration",
+  "required": "Required",
+  "notRequired": "Not Required",
+  "packageStartTime": "Package Start Time",
+  "usageTimeCalculatedFromSignal": "Usage time is calculated from the moment a signal is received",
+  "usageTimeCalculatedFromPurchase": "Usage time is calculated from the moment of purchase",
+  "coverageArea": "Coverage Area",
+  "deliveryMethod": "Delivery Method",
+  "youWillReceiveAnEmailImmediatelyAfterPaymentWithEsimWaitForShippingAndReceiveYourPhysicalSim": "You will receive an email immediately after payment with eSIM. Wait for shipping and receive your physical SIM",
+  "physicalSimWillBeShippedToYourAddressWithin3-5BusinessDays": "Physical SIM will be shipped to your address within 3-5 business days",
+  "wifiHotspot": "Wi-Fi Hotspot",
+  "yesSupported": "Yes (Supported)",
+  "no": "No",
+  "networkTechnology": "Network Technology",
+  "dataResetTime": "Data Reset Time",
+  "timeResetInfo": "One day is calculated until 11:59 PM Local Time",
+  "unlimitedPolicy": "Unlimited Plan",
+  "appSupport": "App Support",
+  "specifications": "Specifications",
+  "notes": "Notes",
+  "note1": "It is recommended to install the eSIM before departure (an internet connection is required during installation), enable Data Roaming, and switch mobile data usage to the GetGo eSIM.",
+  "note2": "GetGo recommends purchasing at least 1 extra day beyond your expected usage to avoid running out of service days due to time zone differences between countries.",
+  "note3": "Each eSIM can only be activated once.",
+  "note4": "The eSIM activation waiting period is 30 days from the time the order is successfully placed.",
+  "note5": "Warranty policy: + 100% replacement or refund guaranteed in case of supplier-related issues + Replacement within 1 hour if issues occur during activation or usage",
+  "note6": "Access to TikTok and ChatGPT may be restricted depending on the device, system compatibility, and current Internet control policies in China.",
+  "internationalTravelStarts": "International travel starts from only 25,000 VND, saving up to 80% compared to traditional Roaming.",
+  "easilyChooseSuitableSim": "Easily choose the suitable SIM or eSIM for various devices.",
+  "eSIMReceiveQR": "eSIM: Receive QR via email, scan and install within 02 minutes to start using immediately.",
+  "physicalSIMDelivery": "Physical SIM: Fast nationwide delivery, compatible with all devices.",
+  "worldLeadingNetwork": "World-leading network infrastructure ensures stable connection anytime, anywhere.",
+  "flexibleDiversePackages": "Flexible and diverse packages to meet all your travel needs.",
+  "customerServiceAvailable": "Our Customer Service team is available 24/7 throughout your journey.",
+  "supportChannels": "Support is available via multiple channels like Hotline, Zalo OA, and WhatsApp.",
+  "commitmentPeaceOfMind": "Commitment to absolute peace of mind and transparent, reliable service experience.",
+  "partnershipNetwork": "Official partner of global corporations",
+  "physicalSim": "Physical SIM",
+  "welcomeBack": "Welcome Back!",
+  "stayConnected": "Stay connected everywhere with Getgo.",
+  "emailAddress": "Email Address",
+  "login": "Login",
+  "orContinueWith": "Or continue with",
+  "checkYourEmail": "Check your email",
+  "weveSentVerificationCode": "We've sent a 6-digit verification code to",
+  "verificationCode": "Verification Code",
+  "codeExpiredPleaseRequestNewOne": "Code expired. Please request a new one.",
+  "fastSimpleRed": "Fast. Simple. Red.",
+  "globalConnectivityForTheModernTraveler": "Global connectivity for the modern traveler.",
+  "codeExpiresIn": "Code expires in",
+  "enterYourEmail": "Enter your email",
+  "searchCountry": "Search country...",
+  "suggestionsEsim": "eSIM suggestions for popular travel routes from",
+  "support": "Support",
+  "travelEsimSim": "What is travel eSIM/SIM?",
+  "physicalTravelSim": "Physical travel SIM",
+  "howToBuyTravelEsimSim": "How to buy travel eSIM/SIM?",
+  "installationGuide": "eSIM installation and activation",
+  "emailAndEsimQrCode": "Email and eSIM QR code",
+  "installationGuideForIphoneIos": "Installation guide for iPhone (iOS)",
+  "installationGuideForAndroid": "Installation guide for Android",
+  "buyNow": "Buy now"
 }

+ 69 - 6
EsimLao/esim-vite/src/i18n/locales/vi.json

@@ -10,23 +10,23 @@
   "orderSearch": "Tra cứu đơn hàng",
   "seeMore": "Xem thêm",
   "whatUs": "Khách hàng nói gì về chúng tôi",
-  "whyGetgo": "Vì sao chọn Getgo",
+  "chooseGetgo": "Vì sao chọn Getgo",
   "fastCost": "Nhanh – Tiện lợi – Chi phí hợp lý",
   "globalAnywhere": "Kết nối toàn cầu, ổn định mọi nơi",
   "customerSupport": "Hỗ trợ khách hàng 24/7",
   "customerPolicy": "Chính sách ưu tiên khách hàng",
-  "getgoWith": "Getgo hợp tác cùng",
+  "getgoWith": "hợp tác cùng",
   "buyExplore": "Mua SIM dễ dàng, sẵn sàng khám phá",
   "compatibleDevice": "Thiết bị tương thích",
   "pickPlan": "Chọn gói cước",
   "instantActivation": "Kích hoạt ngay lập tức",
   "frequentlyQuestions": "Câu hỏi thường gặp",
-  "100% refundDefective": "Hoàn tiền 100% nếu sản phẩm bị lỗi",
+  "refundDefective": "Hoàn tiền 100% nếu sản phẩm bị lỗi",
   "returnGuide": "Hướng dẫn đổi trả & hoàn tiền",
   "chooseTo": "Chọn quốc gia bạn sắp đến",
   "doesEsim?": "Điện thoại của tôi có hỗ trợ eSIM không?",
-  "ordersOrders": "Thông tin đơn hàng",
-  "logoutLogout": "Đăng xuất",
+  "orders": "Thông tin đơn hàng",
+  "logout": "Đăng xuất",
   "viettechLimited": "Công ty TNHH Phát triển toàn cầu VIETTECH",
   "taxCode": "Mã số thuế: 0901210362",
   "address": "218 Đảo Dừa 1, Khu đô thị Vinhomes Ocean Park 2, Xã Nghĩa Trụ, Tỉnh Hưng Yên, Việt Nam",
@@ -143,5 +143,68 @@
   "scouringTheWebForRealTimeInformation": "Đang tìm kiếm thông tin thời gian thực trên web...",
   "news": "Tin tức",
   "contact": "Liên hệ",
-  "home": "Trang chủ"
+  "home": "Trang chủ",
+  "productInformation": "Thông tin sản phẩm",
+  "networkProvider": "Nhà cung cấp mạng",
+  "infoRegistration": "Đăng ký thông tin",
+  "required": "Bắt buộc",
+  "notRequired": "Không bắt buộc",
+  "packageStartTime": "Thời gian bắt đầu gói",
+  "usageTimeCalculatedFromSignal": "Thời gian sử dụng được tính từ khi có tín hiệu",
+  "usageTimeCalculatedFromPurchase": "Thời gian sử dụng được tính từ lúc mua hàng",
+  "coverageArea": "Khu vực phủ sóng",
+  "deliveryMethod": "Phương thức giao hàng",
+  "youWillReceiveAnEmailImmediatelyAfterPaymentWithEsimWaitForShippingAndReceiveYourPhysicalSim": "Bạn sẽ nhận được email ngay sau khi thanh toán với eSIM. Chờ giao hàng và nhận SIM vật lý của bạn",
+  "physicalSimWillBeShippedToYourAddressWithin3-5BusinessDays": "SIM vật lý sẽ được gửi đến địa chỉ của bạn trong vòng 3-5 ngày làm việc",
+  "wifiHotspot": "Điểm truy cập Wi-Fi",
+  "yesSupported": "Có (Hỗ trợ)",
+  "no": "Không",
+  "networkTechnology": "Công nghệ mạng",
+  "dataResetTime": "Thời gian đặt lại dữ liệu",
+  "timeResetInfo": "Một ngày được tính đến 23:59 Giờ địa phương",
+  "unlimitedPolicy": "Gói không giới hạn",
+  "appSupport": "Hỗ trợ ứng dụng",
+  "specifications": "Thông số kỹ thuật",
+  "notes": "Ghi chú",
+  "note1": "Khuyến nghị cài đặt eSIM trước khi khởi hành (cần kết nối internet trong quá trình cài đặt), bật Dữ liệu Di động và chuyển việc sử dụng dữ liệu di động sang eSIM GetGo.",
+  "note2": "GetGo khuyến nghị mua ít nhất 1 ngày bổ sung ngoài thời gian sử dụng dự kiến của bạn để tránh hết ngày dịch vụ do sự khác biệt múi giờ giữa các quốc gia.",
+  "note3": "Mỗi eSIM chỉ có thể được kích hoạt một lần.",
+  "note4": "Thời gian chờ kích hoạt eSIM là 30 ngày kể từ khi đơn hàng được đặt thành công.",
+  "note5": "Chính sách bảo hành: <br />+ Đảm bảo thay thế hoặc hoàn tiền 100% trong trường hợp có sự cố liên quan đến nhà cung cấp <br />+ Thay thế trong vòng 1 giờ nếu xảy ra sự cố trong quá trình kích hoạt hoặc sử dụng",
+  "note6": "Truy cập TikTok và ChatGPT có thể bị hạn chế tùy thuộc vào thiết bị, khả năng tương thích hệ thống và các chính sách kiểm soát Internet hiện tại ở Trung Quốc.",
+  "internationalTravelStarts": "Du lịch quốc tế bắt đầu chỉ từ 25.000 VND, tiết kiệm tới 80% so với Roaming truyền thống.",
+  "easilyChooseSuitableSim": "Dễ dàng chọn SIM phù hợp cho nhiều thiết bị.",
+  "eSIMReceiveQR": "eSIM: Nhận mã QR qua email, quét và cài đặt trong vòng 02 phút để bắt đầu sử dụng ngay lập tức.",
+  "physicalSIMDelivery": "SIM vật lý: Giao hàng nhanh toàn quốc, tương thích với tất cả các thiết bị.",
+  "worldLeadingNetwork": "Hạ tầng mạng hàng đầu thế giới đảm bảo kết nối ổn định mọi lúc, mọi nơi.",
+  "flexibleDiversePackages": "Gói cước linh hoạt và đa dạng đáp ứng mọi nhu cầu du lịch của bạn.",
+  "customerServiceAvailable": "Đội ngũ Chăm sóc Khách hàng của chúng tôi luôn sẵn sàng 24/7 trong suốt hành trình của bạn.",
+  "supportChannels": "Hỗ trợ qua nhiều kênh như Hotline, Zalo OA và WhatsApp.",
+  "commitmentPeaceOfMind": "Cam kết mang lại sự an tâm tuyệt đối và trải nghiệm dịch vụ minh bạch, tin cậy.",
+  "partnershipNetwork": "Đối tác chính thức của các tập đoàn toàn cầu",
+  "physicalSim": "SIM Vật Lý",
+  "welcomeBack": "Chào mừng bạn trở lại!",
+  "stayConnected": "Giữ kết nối mọi nơi với Getgo.",
+  "emailAddress": "Địa chỉ Email",
+  "login": "Đăng nhập",
+  "orContinueWith": "Hoặc tiếp tục với",
+  "checkYourEmail": "Kiểm tra email của bạn",
+  "weveSentVerificationCode": "Chúng tôi đã gửi mã xác minh gồm 6 chữ số đến",
+  "verificationCode": "Mã xác minh",
+  "codeExpiredPleaseRequestNewOne": "Mã đã hết hạn. Vui lòng yêu cầu mã mới.",
+  "fastSimpleRed": "Nhanh. Đơn giản. Đỏ.",
+  "globalConnectivityForTheModernTraveler": "Kết nối toàn cầu cho người du lịch hiện đại",
+  "codeExpiresIn": "Mã hết hạn sau",
+  "enterYourEmail": "Nhập email của bạn",
+  "searchCountry": "Tìm kiếm quốc gia...",
+  "suggestionsEsim": "Gợi ý eSIM cho các tuyến du lịch phổ biến từ",
+  "support": "Hỗ trợ",
+  "travelEsimSim": "eSIM/SIM du lịch là gì?",
+  "physicalTravelSim": "SIM du lịch vật lý",
+  "howToBuyTravelEsimSim": "Cách mua eSIM/SIM du lịch",
+  "installationGuide": "Hướng dẫn cài đặt và kích hoạt eSIM",
+  "emailAndEsimQrCode": "Email và mã QR eSIM",
+  "installationGuideForIphoneIos": "Hướng dẫn cài đặt cho iPhone (iOS)",
+  "installationGuideForAndroid": "Hướng dẫn cài đặt cho Android",
+  "buyNow": "Mua ngay"
 }

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

@@ -13,4 +13,8 @@
 
 .font-black{
   font-weight: 700 !important;
+}
+
+.color-EE0434 {
+  color: #EE0434;
 }

+ 1 - 1
EsimLao/esim-vite/src/pages/contact/ContactView.tsx

@@ -59,7 +59,7 @@ const ContactView: React.FC = () => {
                 </label>
                 <textarea
                   rows={6}
-                  placeholder="Describe your requirements"
+                  placeholder={t("describeRequirements")}
                   className="w-full bg-white border border-slate-200 rounded-2xl py-4 px-6 focus:outline-none focus:ring-2 focus:ring-red-50 focus:border-[#EE0434] transition-all"
                 />
               </div>

+ 22 - 55
EsimLao/esim-vite/src/pages/home/HomeView.tsx

@@ -123,7 +123,7 @@ const HomeView: React.FC = () => {
       <section className="py-16 md:py-24 bg-white px-4">
         <div className="max-w-7xl mx-auto">
           <h2 className="text-3xl md:text-6xl font-black text-center mb-12 md:mb-20 tracking-tight text-slate-900">
-            Why <span className="text-[#EE0434]">choose Getgo</span>
+            <span className="text-[#EE0434]">{t("chooseGetgo")}</span>
           </h2>
           <div className="bg-gradient-to-br from-[#E21c34] to-[#500B28] rounded-[32px] md:rounded-[60px] p-8 md:p-16 lg:p-24 shadow-2xl relative overflow-hidden">
             <div className="absolute top-0 right-0 w-[500px] h-[500px] bg-white/5 rounded-full blur-[100px] -translate-y-1/2 translate-x-1/2"></div>
@@ -148,25 +148,14 @@ const HomeView: React.FC = () => {
                     </svg>
                   </div>
                   <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
-                    Fast – Convenient – Reasonable Cost
+                    {t("fastCost")}
                   </h3>
                 </div>
                 <ul className="space-y-4 text-sm md:text-lg text-white/90 list-disc pl-6 marker:text-red-200">
-                  <li>
-                    International travel starts from only 25,000 VND, saving up
-                    to 80% compared to traditional Roaming.
-                  </li>
-                  <li>
-                    Easily choose the suitable SIM or eSIM for various devices.
-                  </li>
-                  <li>
-                    eSIM: Receive QR via email, scan and install within 02
-                    minutes to start using immediately.
-                  </li>
-                  <li>
-                    Physical SIM: Fast nationwide delivery, compatible with all
-                    devices.
-                  </li>
+                  <li>{t("internationalTravelStarts")}</li>
+                  <li>{t("easilyChooseSuitableSim")}</li>
+                  <li>{t("eSIMReceiveQR")}</li>
+                  <li>{t("physicalSIMDelivery")}</li>
                 </ul>
               </div>
 
@@ -188,22 +177,13 @@ const HomeView: React.FC = () => {
                     </svg>
                   </div>
                   <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
-                    Global Connection, Stable Anywhere
+                    {t("globalAnywhere")}
                   </h3>
                 </div>
                 <ul className="space-y-4 text-sm md:text-lg text-white/90 list-disc pl-6 marker:text-red-200">
-                  <li>
-                    Enjoy ultra-fast 4G/5G speeds for smooth web browsing,
-                    YouTube, Facebook, and Google Maps.
-                  </li>
-                  <li>
-                    World-leading network infrastructure ensures stable
-                    connection anytime, anywhere.
-                  </li>
-                  <li>
-                    Flexible and diverse packages, suitable for personal,
-                    business, or travel needs.
-                  </li>
+                  <li>{t("fastestData")}</li>
+                  <li>{t("worldLeadingNetwork")}</li>
+                  <li>{t("flexibleDiversePackages")}</li>
                 </ul>
               </div>
 
@@ -225,18 +205,12 @@ const HomeView: React.FC = () => {
                     </svg>
                   </div>
                   <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
-                    24/7 Customer Support
+                    {t("customerSupport")}
                   </h3>
                 </div>
                 <ul className="space-y-4 text-sm md:text-lg text-white/90 list-disc pl-6 marker:text-red-200">
-                  <li>
-                    Our Customer Service team is available 24/7 throughout your
-                    journey.
-                  </li>
-                  <li>
-                    Support is available via multiple channels like Hotline,
-                    Zalo OA, and WhatsApp.
-                  </li>
+                  <li>{t("customerServiceAvailable")}</li>
+                  <li>{t("supportChannels")}</li>
                 </ul>
               </div>
 
@@ -258,18 +232,12 @@ const HomeView: React.FC = () => {
                     </svg>
                   </div>
                   <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
-                    Customer-First Policy
+                    {t("customerPolicy")}
                   </h3>
                 </div>
                 <ul className="space-y-4 text-sm md:text-lg text-white/90 list-disc pl-6 marker:text-red-200">
-                  <li>
-                    Exchange for a new product within 1 hour or 100% refund if
-                    there are technical errors.
-                  </li>
-                  <li>
-                    Commitment to absolute peace of mind and transparent,
-                    reliable service experience.
-                  </li>
+                  <li>{t("refundDefective")}</li>
+                  <li>{t("commitmentPeaceOfMind")}</li>
                 </ul>
               </div>
             </div>
@@ -281,7 +249,7 @@ const HomeView: React.FC = () => {
       <section className="py-12 md:py-20 bg-white px-4">
         <div className="max-w-7xl mx-auto space-y-10 md:space-y-16">
           <h2 className="text-3xl md:text-6xl font-black text-center tracking-tight text-slate-900">
-            Getgo has <span className="text-[#EE0434]">partnered with</span>
+            Getgo <span className="text-[#EE0434]">{t("getgoWith")}</span>
           </h2>
 
           <div className="space-y-8 md:space-y-12 max-w-5xl mx-auto">
@@ -301,7 +269,7 @@ const HomeView: React.FC = () => {
                   </svg>
                 </div>
                 <p className="text-sm md:text-2xl font-semibold text-slate-500">
-                  Official partner of global corporations
+                  {t("partnershipNetwork")}
                 </p>
               </div>
               <div className="flex flex-wrap gap-3 md:gap-8 justify-center lg:justify-start">
@@ -327,7 +295,7 @@ const HomeView: React.FC = () => {
       <section className="py-12 md:py-24 bg-white px-4">
         <div className="max-w-7xl mx-auto">
           <h2 className="text-2xl md:text-6xl font-black text-[#EE0434] text-center mb-6 md:mb-10 tracking-tight leading-tight">
-            Buy a SIM easily, <br className="md:hidden" /> ready to explore
+            {t("buyExplore")}
           </h2>
           <div className="flex justify-center mb-10 md:mb-20">
             <div className="inline-flex p-1 bg-slate-100 rounded-full shadow-inner">
@@ -349,7 +317,7 @@ const HomeView: React.FC = () => {
                     : "bg-[#f0f0f0] text-[#8b8e96] hover:text-slate-800"
                 }`}
               >
-                Physical SIM
+                {t("physicalSim")}
               </button>
             </div>
           </div>
@@ -403,8 +371,7 @@ const HomeView: React.FC = () => {
       <section className="py-16 md:py-24 bg-white px-4">
         <div className="max-w-7xl mx-auto flex flex-col items-center">
           <h2 className="text-3xl md:text-[56px] font-black text-center mb-12 md:mb-20 tracking-tight text-slate-900">
-            <span className="text-[#EE0434]">100%</span> refund policy if
-            product is defective
+            {t("refundDefective")}
           </h2>
           <button className="flex items-center bg-gradient-to-r from-[#E21c34] to-[#500B28] p-2 pr-10 rounded-full shadow-xl hover:scale-105 transition-all">
             <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center text-slate-800">
@@ -423,7 +390,7 @@ const HomeView: React.FC = () => {
               </svg>
             </div>
             <span className="ml-6 text-white text-2xl font-black">
-              Return and Refund Guide
+              {t("returnGuide")}
             </span>
           </button>
         </div>

+ 15 - 13
EsimLao/esim-vite/src/pages/login/LoginView.tsx

@@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom";
 import { useEffect, useRef } from "react";
 import { accountLogin } from "../../features/account/accuntSlice";
 import logoApp from "../../assets/img/logo_white.png";
+import { useTranslation } from "react-i18next";
 
 const LoginView: React.FC = () => {
   const navigate = useNavigate();
@@ -25,6 +26,7 @@ const LoginView: React.FC = () => {
   const intervalRef = useRef<NodeJS.Timeout | null>(null);
   const inputRef = useRef<HTMLInputElement>(null);
   const [errorMessage, setErrorMessage] = useState("");
+  const { t } = useTranslation();
   let interval;
 
   useEffect(() => {
@@ -218,10 +220,10 @@ const LoginView: React.FC = () => {
               </div>
             </div>
             <h1 className="text-3xl md:text-4xl lg:text-5xl font-black text-slate-900 tracking-tight">
-              Welcome Back!
+              {t("welcomeBack")}
             </h1>
             <p className="text-slate-400 font-medium text-base md:text-lg">
-              Stay connected everywhere with Getgo.
+              {t("stayConnected")}
             </p>
           </div>
           {step === "email" && (
@@ -229,13 +231,13 @@ const LoginView: React.FC = () => {
               <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
+                    {t("emailAddress")}
                   </label>
                   <input
                     type="email"
                     value={email}
                     onChange={(e) => setEmail(e.target.value)}
-                    placeholder="Enter your email"
+                    placeholder={t("enterYourEmail")}
                     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>
@@ -258,7 +260,7 @@ const LoginView: React.FC = () => {
                   {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>
+                  <span>{t("login")}</span>
                 </button>
               </form>
               <div className="space-y-6">
@@ -268,7 +270,7 @@ const LoginView: React.FC = () => {
                   </div>
                   <div className="relative flex justify-center text-sm">
                     <span className="px-4 bg-white text-slate-400 font-bold uppercase tracking-wider text-[10px]">
-                      Or continue with
+                      {t("orContinueWith")}
                     </span>
                   </div>
                 </div>
@@ -327,10 +329,10 @@ const LoginView: React.FC = () => {
               <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
+                    {t("checkYourEmail")}
                   </h1>
                   <p className="text-slate-400 font-medium text-base md:text-lg">
-                    We've sent a 6-digit verification code to <br />
+                    {t("weveSentVerificationCode")} <br />
                     <span className="text-slate-800 font-bold">{email}</span>
                   </p>
                 </div>
@@ -338,7 +340,7 @@ const LoginView: React.FC = () => {
                 <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
+                      {t("verificationCode")}
                     </label>
 
                     <div
@@ -411,12 +413,12 @@ const LoginView: React.FC = () => {
                 <div className="text-center">
                   {timer > 0 ? (
                     <p className="text-slate-500 font-medium">
-                      Code expires in{" "}
+                      {t("codeExpiresIn")}{" "}
                       <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.
+                      {t("codeExpiredPleaseRequestNewOne")}
                     </p>
                   )}
                 </div>
@@ -427,9 +429,9 @@ const LoginView: React.FC = () => {
       </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>
+          <h2 className="text-5xl font-black">{t("fastSimpleRed")}</h2>
           <p className="text-xl opacity-80">
-            Global connectivity for the modern traveler.
+            {t("globalConnectivityForTheModernTraveler")}
           </p>
         </div>
       </div>

+ 92 - 37
EsimLao/esim-vite/src/pages/product-detail/ProductDetailView.tsx

@@ -1,5 +1,5 @@
 import React, { useState, useMemo, useEffect } from "react";
-import { useLocation, useNavigate, Link } from "react-router-dom";
+import { useLocation, useNavigate, Link, useParams } from "react-router-dom";
 import { SelectedProduct } from "../../services/types";
 import { Area, Package } from "../../services/product/type";
 import { useMutation, useQuery } from "@tanstack/react-query";
@@ -8,14 +8,19 @@ import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
 import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
 import { productApi } from "../../apis/productApi";
 import { openPopup } from "../../features/popup/popupSlice";
-import { get } from "http";
 import { formatNumber } from "../../logic/loigicUtils";
+import ProductInfoModal from "../../components/ProductInfoModal";
+import PackageOverview from "./components/PackageOverview";
+import ProductCard from "../../components/ProductCard";
+import { useTranslation } from "react-i18next";
 
 const ProductDetailView: React.FC = () => {
   const location = useLocation();
   const navigate = useNavigate();
   const dispatch = useAppDispatch();
+  const { t } = useTranslation();
   const area = location.state as Area;
+  const { id } = useParams<{ id: string }>();
   const loading = useAppSelector((state) => state.loading);
   const [selectedDays, setSelectedDays] = useState<number>(null);
   const [selectedData, setSelectedData] = useState<string>("Unlimited");
@@ -23,6 +28,7 @@ const ProductDetailView: React.FC = () => {
   const [dataOptions, setDataOptions] = useState<string[]>([]);
   const [daysActiveOptions, setDaysActiveOptions] = useState<number[]>([]);
   const [dataActiveOptions, setDataActiveOptions] = useState<string[]>([]);
+  const [relatedAreas, setRelatedAreas] = useState<Area[]>([]);
   const [prices, setPrices] = useState<{
     original: string;
     final: string;
@@ -36,7 +42,9 @@ const ProductDetailView: React.FC = () => {
   const [quantity, setQuantity] = useState<number>(1);
   const [packages, setPackages] = useState<Package[]>([]);
 
-  if (!area) {
+  const [selectedPackage, setSelectedPackage] = useState<Package | null>(null);
+
+  if (!id) {
     return (
       <div className="min-h-screen flex items-center justify-center">
         <div className="text-center">
@@ -49,38 +57,41 @@ const ProductDetailView: React.FC = () => {
     );
   }
 
-  // const { data: loadPackage = [] } = useQuery<Package[]>({
-  //   queryKey: [DataCacheKey.PACKAGES],
-  //   queryFn: async (): Promise<Package[]> => {
-  //     try {
-  //       dispatch(startLoading({}));
-  //       const res = await productApi.loadPackage({
-  //         areaId: area.id,
-  //         isUnlimited: "-1",
-  //         isDaily: "-1",
-  //       });
-  //       return res.data as Package[];
-  //     } catch (error) {
-  //       console.error(error);
-  //       return []; // 🔴 bắt buộc
-  //     } finally {
-  //       dispatch(stopLoading());
-  //     }
-  //   },
-  //   staleTime: staleTime,
-  // });
-
   useEffect(() => {
     getProductMutation.mutate();
-  }, [area]);
+    getRelatedAreaMutation.mutate();
+  }, [id]);
 
   const getProductMutation = useMutation({
     mutationFn: async () => {
       dispatch(startLoading({}));
       const res = await productApi.loadPackage({
-        areaId: area.id,
-        isUnlimited: "-1",
-        isDaily: "-1",
+        areaId: id,
+        dataType: "-1",
+      });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopLoading());
+      console.log("Get package response data:", data);
+      if (data && data.errorCode === "0") {
+        const packages = data.data as Package[];
+        setPackages(packages);
+      } else {
+        console.error("Get package failed, no token received");
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopLoading());
+      console.error("Get package error:", error.response.data);
+    },
+  });
+
+  const getRelatedAreaMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await productApi.loadRelatedArea({
+        areaId: id,
       });
       return res;
     },
@@ -88,8 +99,8 @@ const ProductDetailView: React.FC = () => {
       dispatch(stopLoading());
       console.log("Get package response data:", data);
       if (data && data.errorCode === "0") {
-        console.log("Get package successful");
-        setPackages(data.data as Package[]);
+        const areas = data.data as Area[];
+        setRelatedAreas(areas);
       } else {
         console.error("Get package failed, no token received");
       }
@@ -130,6 +141,8 @@ const ProductDetailView: React.FC = () => {
     setDataOptions(options.dataArray);
     handleSelectDay(options.daysArray[0]);
     handleSelectData(options.dataArray[0]);
+    // setSelectedPackage(packages.length > 0 ? packages[0] : null);
+    // console.log("Set package successful", packages.length, selectedPackage);
   }, [options]);
 
   useEffect(() => {
@@ -149,6 +162,13 @@ const ProductDetailView: React.FC = () => {
       dataSet.has(selectedData) ? selectedData : Array.from(dataSet)[0]
     );
     setDataActiveOptions(Array.from(dataSet));
+    setSelectedPackage(
+      packages.find(
+        (p) =>
+          p.dayDuration === selectedDays &&
+          p.amountData.toString() === selectedData
+      )
+    );
   };
 
   const handleSelectData = (data: string) => {
@@ -164,16 +184,25 @@ const ProductDetailView: React.FC = () => {
       daysSet.has(selectedDays) ? selectedDays : Array.from(daysSet)[0]
     );
     setDaysActiveOptions(Array.from(daysSet));
+    setSelectedPackage(
+      packages.find(
+        (p) =>
+          p.dayDuration === selectedDays &&
+          p.amountData.toString() === selectedData
+      )
+    );
   };
 
   const getPrices = (quantityParam?: number) => {
     const quantityToUse =
       quantityParam !== undefined ? quantityParam : quantity;
     // find package based on selectedDays and selectedData
-    const selectedPackage = packages.find(
-      (p) =>
-        p.dayDuration === selectedDays &&
-        p.amountData.toString() === selectedData
+    setSelectedPackage(
+      packages.find(
+        (p) =>
+          p.dayDuration === selectedDays &&
+          p.amountData.toString() === selectedData
+      )
     );
     if (!selectedPackage) {
       console.log(
@@ -261,7 +290,7 @@ const ProductDetailView: React.FC = () => {
             to="/"
             className="hover:text-[#EE0434] transition-colors text-[18px]"
           >
-            Home
+            {t("home")}
           </Link>
           <svg
             className="w-3 h-3"
@@ -308,6 +337,9 @@ const ProductDetailView: React.FC = () => {
               </div>
             </div>
           </div>
+
+          {/* Product Information Card */}
+          <PackageOverview packageInfo={selectedPackage} />
         </div>
 
         <div className="lg:col-span-7 space-y-8 md:space-y-10 pt-4">
@@ -357,7 +389,7 @@ const ProductDetailView: React.FC = () => {
                       : "border-slate-100 text-slate-300"
                   }`}
                 >
-                  {data === "0" ? "Unlimited" : data + " MB"}
+                  {data === "0" ? "Unlimited" : data + " GB"}
                 </button>
               ))}
             </div>
@@ -441,9 +473,32 @@ const ProductDetailView: React.FC = () => {
               {loading.isSmallLoading && (
                 <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
               )}
-              <span>Buy now</span>
+              <span>{t("buyNow")}</span>
             </button>
           </div>
+
+          {/* Suggestions Section */}
+          <div className="mt-12">
+            <h3 className="text-xl md:text-2xl font-black text-slate-900 mb-6">
+              {t("suggestionsEsim")} {area.areaName1}:
+            </h3>
+            <div className="grid grid-cols-2 gap-3 md:gap-4">
+              {relatedAreas.map((item) => (
+                <ProductCard
+                  key={item.id}
+                  p={item}
+                  onClick={() => {
+                    navigate(`/product/${item.id}`, {
+                      state: {
+                        ...item,
+                      },
+                    });
+                    window.scrollTo({ top: 0, behavior: "smooth" });
+                  }}
+                />
+              ))}
+            </div>
+          </div>
         </div>
       </div>
     </div>

+ 83 - 0
EsimLao/esim-vite/src/pages/product-detail/components/PackageOverview.tsx

@@ -0,0 +1,83 @@
+import { useTranslation } from "react-i18next";
+import ProductInfoModal from "../../../components/ProductInfoModal";
+import { Package } from "../../../services/product/type";
+import React, { useState } from "react";
+
+const PackageOverview = ({ packageInfo }: { packageInfo: Package }) => {
+  const { t } = useTranslation();
+  const [isInfoModalOpen, setIsInfoModalOpen] = useState<boolean>(false);
+  return (
+    <>
+      <ProductInfoModal
+        isOpen={isInfoModalOpen}
+        onClose={() => setIsInfoModalOpen(false)}
+        packageInfo={packageInfo}
+      />
+      <div className="bg-[#f0f9ff] rounded-[24px] p-6 md:p-8 border border-sky-100 shadow-sm transition-all">
+        <h3 className="text-lg md:text-xl font-black text-slate-900 mb-4">
+          {t("productInformation")}:
+        </h3>
+        <ul className="space-y-4 text-sm md:text-base">
+          <li className="">
+            <div className="font-bold text-slate-500 min-w-[150px] shrink-0">
+              {t("networkProvider")}:
+            </div>
+            <div className="font-bold text-slate-900">
+              {packageInfo?.carriers}
+            </div>
+          </li>
+          <li className="flex flex-col sm:flex-row sm:items-baseline gap-1 sm:gap-2">
+            <span className="font-bold text-slate-500 min-w-[150px] shrink-0">
+              {t("infoRegistration")} (eKYC):
+            </span>
+            <span className="font-medium text-slate-900">
+              {packageInfo?.ekycRequired ? t("required") : t("notRequired")}
+            </span>
+          </li>
+          <li className="flex flex-col sm:flex-row sm:items-baseline gap-1 sm:gap-2">
+            <span className="font-bold text-slate-500 min-w-[150px] shrink-0">
+              {t("packageStartTime")}:
+            </span>
+            <span className="font-medium text-slate-900">
+              {packageInfo?.activationMethod === 1
+                ? t("usageTimeCalculatedFromSignal")
+                : t("usageTimeCalculatedFromPurchase")}
+            </span>
+          </li>
+          <li className="flex flex-col sm:flex-row sm:items-baseline gap-1 sm:gap-2">
+            <span className="font-bold text-slate-500 min-w-[150px] shrink-0">
+              {t("coverageArea")}:
+            </span>
+            <span className="font-medium text-slate-900">
+              {packageInfo?.coverageArea}
+            </span>
+          </li>
+        </ul>
+
+        <button
+          onClick={() => setIsInfoModalOpen(true)}
+          className="mt-6 flex items-center text-[#EE0434] font-bold text-sm hover:underline hover:text-[#BB0329] transition-colors group"
+        >
+          <span>{t("seeMore")}</span>
+          <div className="w-5 h-5 bg-white rounded-full flex items-center justify-center ml-2 shadow-sm group-hover:shadow-md transition-all">
+            <svg
+              className="w-3 h-3 text-[#EE0434]"
+              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>
+          </div>
+        </button>
+      </div>
+    </>
+  );
+};
+
+export default PackageOverview;

+ 13 - 11
EsimLao/esim-vite/src/pages/support/SupportView.tsx

@@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
 import { Link } from "react-router-dom";
 
 const SupportView: React.FC = () => {
-  const [activeItem, setActiveItem] = useState("What is travel eSIM/SIM?");
   const { t } = useTranslation();
+  const [activeItem, setActiveItem] = useState(t("travelEsimSim"));
   const categories = [
     {
-      title: "What is travel eSIM/SIM?",
+      title: t("travelEsimSim"),
       count: 3,
       icon: (
         <svg
@@ -25,14 +25,14 @@ const SupportView: React.FC = () => {
         </svg>
       ),
       items: [
-        "What is travel eSIM/SIM",
-        "Physical travel SIM",
-        "How to buy travel eSIM/SIM",
+        t("travelEsimSim"),
+        t("physicalTravelSim"),
+        t("howToBuyTravelEsimSim"),
       ],
       color: "bg-red-500",
     },
     {
-      title: "eSIM installation and activation",
+      title: t("installationGuide"),
       count: 3,
       icon: (
         <svg
@@ -50,9 +50,9 @@ const SupportView: React.FC = () => {
         </svg>
       ),
       items: [
-        "Email and eSIM QR code",
-        "Installation guide for iPhone (iOS)",
-        "Installation guide for Android",
+        t("emailAndEsimQrCode"),
+        t("installationGuideForIphoneIos"),
+        t("installationGuideForAndroid"),
       ],
       color: "bg-red-600",
     },
@@ -63,7 +63,7 @@ const SupportView: React.FC = () => {
       <div className="max-w-7xl mx-auto px-4 py-6">
         <nav className="flex items-center space-x-2 text-sm text-slate-500 font-medium">
           <Link to="/" className="hover:text-[#EE0434] text-[18px]">
-            Home
+            {t("home")}
           </Link>
           <svg
             className="w-4 h-4"
@@ -78,7 +78,9 @@ const SupportView: React.FC = () => {
               strokeLinejoin="round"
             />
           </svg>
-          <span className="text-slate-900 font-bold text-[18px]">Support</span>
+          <span className="text-slate-900 font-bold text-[18px]">
+            {t("support")}
+          </span>
         </nav>
       </div>
 

+ 51 - 15
EsimLao/esim-vite/src/services/product/type.ts

@@ -16,26 +16,62 @@ export interface Area {
 }
 
 export interface Package {
-  id: number;
-  areaId: number;
-  packageCode: string;
-  displayPrice: number;
+  // id: number;
+  // areaId: number;
+  // packageCode: string;
+  // displayPrice: number;
+  // amountData: number;
+  // dayDuration: number;
+  // isUnlimited: number;
+  // packageName: string;
+  // title: string;
+  // description: string;
+  // moreInfo: string;
+  // sellPrice: number;
+  // apnType: number;
+  // isKycVerify: number;
+  // rechargeability: number;
+  // isLocal: number;
+  // status: number;
+  // priority: number;
+  // limitedPolicy: string;
+  // isDaily: number;
+
+  activationMethod: number;
   amountData: number;
+  apnType: number;
+  appSupport: any;
+  areaId: number;
+  carriers: string;
+  coverageArea: string;
+  currency: string;
+  dataResetTime: number;
+  dataType: number;
+  dataUnit: string;
   dayDuration: number;
-  isUnlimited: number;
-  packageName: string;
-  title: string;
+  deliveryMethod: number;
   description: string;
-  moreInfo: string;
-  sellPrice: number;
-  apnType: number;
-  isKycVerify: number;
-  rechargeability: number;
+  displayPrice: number;
+  ekycRequired: number;
+  geographicType: number;
+  id: number;
   isLocal: number;
-  status: number;
+  isTopup: number;
+  moreInfo: any;
+  networkTechnology: string;
+  note: any;
+  packageCode: string;
+  packageName: string;
+  phoneNumber: number;
   priority: number;
-  limitedPolicy: string;
-  isDaily: number;
+  rechargeability: number;
+  sellPrice: number;
+  simType: number;
+  status: number;
+  title: string;
+  unlimitedPolicy: any;
+  validityStart: number;
+  wifiSharing: number;
 }
 
 export interface CheckoutDetailResponse {