Ver código fonte

Implement localStorage caching for areas and UI fixes

Replaced Redux state for areas with localStorage caching using new utility functions (getWithExpiry, setWithExpiry) to improve performance and persistence. Fixed carrier formatting in PackageOverview, enabled HTML rendering for note5 in ProductInfoModal, corrected price display logic in CheckoutView, and made minor refactoring in ProductDetailView. Updated Vite config to allow specific host.
trunghieubui 3 semanas atrás
pai
commit
441356b691

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

@@ -16,10 +16,12 @@ import { useSelector } from "react-redux";
 import { setAreas } from "../features/areas/areasSlice";
 import i18n from "../i18n";
 import { useTranslation } from "react-i18next";
+import { getWithExpiry, setWithExpiry } from "../logic/loigicUtils";
 
 const Header: React.FC = () => {
   const navigate = useNavigate();
-  const areas = useSelector((state: any) => state.areas.areas);
+  // const areas = useSelector((state: any) => state.areas.areas);
+  const areas = getWithExpiry<Area[] | []>("areas");
   const { t } = useTranslation();
   const location = useLocation();
   const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -153,7 +155,7 @@ const Header: React.FC = () => {
       console.log("Get area response data:", data);
       if (data && data.errorCode === "0") {
         console.log("Get area successful");
-        dispatch(setAreas(data.data as Area[]));
+        setWithExpiry("areas", JSON.stringify(data.data));
         setAreasList(data.data as Area[]);
       } else {
         console.error("Get area failed, no token received");
@@ -181,7 +183,7 @@ const Header: React.FC = () => {
       setAreasList(areas);
     } else {
       const filtered = areas.filter((area: Area) =>
-        area.areaName1.toLowerCase().includes(query.toLowerCase())
+        area.coverageArea.toLowerCase().includes(query.toLowerCase())
       );
       setAreasList(filtered);
     }

+ 2 - 1
EsimLao/esim-vite/src/components/ProductInfoModal.tsx

@@ -361,7 +361,8 @@ const ProductInfoModal: React.FC<ProductInfoModalProps> = ({
               </p>
               <p className="font-medium leading-relaxed">
                 <span className="font-bold text-[16px] color-EE0434">5.</span>{" "}
-                {t("note5")}
+                {/* display html */}
+                <span dangerouslySetInnerHTML={{ __html: t("note5") }} />
               </p>
               <p className="font-medium leading-relaxed">
                 <span className="font-bold text-[16px] color-EE0434">6.</span>{" "}

+ 41 - 2
EsimLao/esim-vite/src/logic/loigicUtils.ts

@@ -1,3 +1,42 @@
 export const formatNumber = (val: number) => {
-    return new Intl.NumberFormat('vi-VN').format(val);
-}
+  return new Intl.NumberFormat("vi-VN").format(val);
+};
+
+export const formatCarriers = (text) => {
+  if (!text) return "";
+  return text.replace(/,\s*/g, ", ");
+};
+
+export const setWithExpiry = (
+  key: string,
+  value: string,
+  ttlMs?: number | null
+) => {
+  const now = Date.now();
+
+  const item = {
+    value,
+    expiry: ttlMs ? now + ttlMs : now + 10 * 60 * 1000, // Default 10 minutes
+  };
+
+  localStorage.setItem(key, JSON.stringify(item));
+};
+
+export const getWithExpiry = <T>(key: string): T | null => {
+  const itemStr = localStorage.getItem(key);
+  if (!itemStr) return null;
+
+  try {
+    const item = JSON.parse(itemStr);
+
+    if (!item.expiry || Date.now() > item.expiry) {
+      localStorage.removeItem(key);
+      return null;
+    }
+
+    return item.value as T;
+  } catch {
+    localStorage.removeItem(key);
+    return null;
+  }
+};

+ 2 - 2
EsimLao/esim-vite/src/pages/checkout/CheckoutView.tsx

@@ -300,11 +300,11 @@ const CheckoutView = () => {
               </div>
               <div className="text-right">
                 <p className="text-xs text-slate-400 line-through font-bold">
-                  {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
+                  {formatNumber(state.checkoutDetails.totalMoney)}{" "}
                   {state.checkoutDetails.curency}
                 </p>
                 <p className="text-[#0071e3] font-black text-xl">
-                  {formatNumber(state.checkoutDetails.totalMoney)}{" "}
+                  {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
                   {state.checkoutDetails.curency}
                 </p>
               </div>

+ 5 - 2
EsimLao/esim-vite/src/pages/home/components/HomeSearch.tsx

@@ -11,9 +11,10 @@ import { setAreas } from "../../../features/areas/areasSlice";
 import { Area } from "../../../services/product/type";
 import { useNavigate } from "react-router";
 import { useTranslation } from "react-i18next";
+import { getWithExpiry, setWithExpiry } from "@/src/logic/loigicUtils";
+import { get } from "http";
 
 const HomeSearch = () => {
-  const areas = useSelector((state: any) => state.areas.areas);
   const dispatch = useDispatch();
   const navigate = useNavigate();
   const { t } = useTranslation();
@@ -21,6 +22,7 @@ const HomeSearch = () => {
   const [isDropdownOpen, setIsDropdownOpen] = useState(false);
   const dropdownRef = useRef<HTMLDivElement>(null);
   const [areasList, setAreasList] = useState<Area[]>([]);
+  const areas = getWithExpiry<Area[] | []>("areas");
 
   useEffect(() => {
     if (!areas || areas.length === 0) getAreaMutation.mutate();
@@ -44,7 +46,8 @@ const HomeSearch = () => {
       console.log("Get area response data:", data);
       if (data && data.errorCode === "0") {
         console.log("Get area successful");
-        dispatch(setAreas(data.data as Area[]));
+        // dispatch(setAreas(data.data as Area[]));
+        setWithExpiry("areas", JSON.stringify(data.data));
         setAreasList(data.data as Area[]);
       } else {
         console.error("Get area failed, no token received");

+ 16 - 21
EsimLao/esim-vite/src/pages/product-detail/ProductDetailView.tsx

@@ -197,21 +197,19 @@ const ProductDetailView: React.FC = () => {
     const quantityToUse =
       quantityParam !== undefined ? quantityParam : quantity;
     // find package based on selectedDays and selectedData
-    setSelectedPackage(
-      packages.find(
-        (p) =>
-          p.dayDuration === selectedDays &&
-          p.amountData.toString() === selectedData
-      )
+    let selectedPackageTmp = packages.find(
+      (p) =>
+        p.dayDuration === selectedDays &&
+        p.amountData.toString() === selectedData
     );
-    if (!selectedPackage) {
-      console.log(
-        "No package found for the selected options " +
-          selectedDays +
-          " days and " +
-          selectedData +
-          " data"
-      );
+    if (!selectedPackageTmp) {
+      // console.log(
+      //   "No package found for the selected options " +
+      //     selectedDays +
+      //     " days and " +
+      //     selectedData +
+      //     " data"
+      // );
       return {
         original: "0.00",
         final: "0.00",
@@ -220,17 +218,14 @@ const ProductDetailView: React.FC = () => {
     }
     console.log(
       "Selected package: ",
-      selectedPackage +
-        " for prices calculation " +
-        selectedPackage.sellPrice +
-        " quantity " +
-        quantityToUse
+      selectedPackageTmp + " quantity " + quantityToUse
     );
     setPrices({
-      original: quantityToUse * selectedPackage.displayPrice,
-      final: quantityToUse * selectedPackage.sellPrice,
+      original: quantityToUse * selectedPackageTmp.displayPrice,
+      final: quantityToUse * selectedPackageTmp.sellPrice,
       discountPercent: "0",
     });
+    setSelectedPackage(selectedPackageTmp);
   };
 
   const handleQuantityChange = (change: number) => {

+ 7 - 2
EsimLao/esim-vite/src/pages/product-detail/components/PackageOverview.tsx

@@ -1,11 +1,16 @@
 import { useTranslation } from "react-i18next";
 import ProductInfoModal from "../../../components/ProductInfoModal";
 import { Package } from "../../../services/product/type";
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
+import { formatCarriers } from "../../../logic/loigicUtils";
 
 const PackageOverview = ({ packageInfo }: { packageInfo: Package }) => {
   const { t } = useTranslation();
   const [isInfoModalOpen, setIsInfoModalOpen] = useState<boolean>(false);
+  useEffect(() => {
+    console.log("Package info updated: ", packageInfo);
+  }, [packageInfo]);
+
   return (
     <>
       <ProductInfoModal
@@ -23,7 +28,7 @@ const PackageOverview = ({ packageInfo }: { packageInfo: Package }) => {
               {t("networkProvider")}:
             </div>
             <div className="font-bold text-slate-900">
-              {packageInfo?.carriers}
+              {formatCarriers(packageInfo?.carriers)}
             </div>
           </li>
           <li className="flex flex-col sm:flex-row sm:items-baseline gap-1 sm:gap-2">

+ 1 - 0
EsimLao/esim-vite/vite.config.ts

@@ -9,6 +9,7 @@ export default defineConfig(({ mode }) => {
     server: {
       port: 3000,
       host: "0.0.0.0",
+      allowedHosts: ["e4578e1fbc59.ngrok-free.app"],
     },
     plugins: [react(), tailwindcss()],
     define: {