Browse Source

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

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

@@ -361,7 +361,8 @@ const ProductInfoModal: React.FC<ProductInfoModalProps> = ({
               </p>
               </p>
               <p className="font-medium leading-relaxed">
               <p className="font-medium leading-relaxed">
                 <span className="font-bold text-[16px] color-EE0434">5.</span>{" "}
                 <span className="font-bold text-[16px] color-EE0434">5.</span>{" "}
-                {t("note5")}
+                {/* display html */}
+                <span dangerouslySetInnerHTML={{ __html: t("note5") }} />
               </p>
               </p>
               <p className="font-medium leading-relaxed">
               <p className="font-medium leading-relaxed">
                 <span className="font-bold text-[16px] color-EE0434">6.</span>{" "}
                 <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) => {
 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>
               <div className="text-right">
               <div className="text-right">
                 <p className="text-xs text-slate-400 line-through font-bold">
                 <p className="text-xs text-slate-400 line-through font-bold">
-                  {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
+                  {formatNumber(state.checkoutDetails.totalMoney)}{" "}
                   {state.checkoutDetails.curency}
                   {state.checkoutDetails.curency}
                 </p>
                 </p>
                 <p className="text-[#0071e3] font-black text-xl">
                 <p className="text-[#0071e3] font-black text-xl">
-                  {formatNumber(state.checkoutDetails.totalMoney)}{" "}
+                  {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
                   {state.checkoutDetails.curency}
                   {state.checkoutDetails.curency}
                 </p>
                 </p>
               </div>
               </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 { Area } from "../../../services/product/type";
 import { useNavigate } from "react-router";
 import { useNavigate } from "react-router";
 import { useTranslation } from "react-i18next";
 import { useTranslation } from "react-i18next";
+import { getWithExpiry, setWithExpiry } from "@/src/logic/loigicUtils";
+import { get } from "http";
 
 
 const HomeSearch = () => {
 const HomeSearch = () => {
-  const areas = useSelector((state: any) => state.areas.areas);
   const dispatch = useDispatch();
   const dispatch = useDispatch();
   const navigate = useNavigate();
   const navigate = useNavigate();
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -21,6 +22,7 @@ const HomeSearch = () => {
   const [isDropdownOpen, setIsDropdownOpen] = useState(false);
   const [isDropdownOpen, setIsDropdownOpen] = useState(false);
   const dropdownRef = useRef<HTMLDivElement>(null);
   const dropdownRef = useRef<HTMLDivElement>(null);
   const [areasList, setAreasList] = useState<Area[]>([]);
   const [areasList, setAreasList] = useState<Area[]>([]);
+  const areas = getWithExpiry<Area[] | []>("areas");
 
 
   useEffect(() => {
   useEffect(() => {
     if (!areas || areas.length === 0) getAreaMutation.mutate();
     if (!areas || areas.length === 0) getAreaMutation.mutate();
@@ -44,7 +46,8 @@ const HomeSearch = () => {
       console.log("Get area response data:", data);
       console.log("Get area response data:", data);
       if (data && data.errorCode === "0") {
       if (data && data.errorCode === "0") {
         console.log("Get area successful");
         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[]);
         setAreasList(data.data as Area[]);
       } else {
       } else {
         console.error("Get area failed, no token received");
         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 =
     const quantityToUse =
       quantityParam !== undefined ? quantityParam : quantity;
       quantityParam !== undefined ? quantityParam : quantity;
     // find package based on selectedDays and selectedData
     // 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 {
       return {
         original: "0.00",
         original: "0.00",
         final: "0.00",
         final: "0.00",
@@ -220,17 +218,14 @@ const ProductDetailView: React.FC = () => {
     }
     }
     console.log(
     console.log(
       "Selected package: ",
       "Selected package: ",
-      selectedPackage +
-        " for prices calculation " +
-        selectedPackage.sellPrice +
-        " quantity " +
-        quantityToUse
+      selectedPackageTmp + " quantity " + quantityToUse
     );
     );
     setPrices({
     setPrices({
-      original: quantityToUse * selectedPackage.displayPrice,
-      final: quantityToUse * selectedPackage.sellPrice,
+      original: quantityToUse * selectedPackageTmp.displayPrice,
+      final: quantityToUse * selectedPackageTmp.sellPrice,
       discountPercent: "0",
       discountPercent: "0",
     });
     });
+    setSelectedPackage(selectedPackageTmp);
   };
   };
 
 
   const handleQuantityChange = (change: number) => {
   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 { useTranslation } from "react-i18next";
 import ProductInfoModal from "../../../components/ProductInfoModal";
 import ProductInfoModal from "../../../components/ProductInfoModal";
 import { Package } from "../../../services/product/type";
 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 PackageOverview = ({ packageInfo }: { packageInfo: Package }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isInfoModalOpen, setIsInfoModalOpen] = useState<boolean>(false);
   const [isInfoModalOpen, setIsInfoModalOpen] = useState<boolean>(false);
+  useEffect(() => {
+    console.log("Package info updated: ", packageInfo);
+  }, [packageInfo]);
+
   return (
   return (
     <>
     <>
       <ProductInfoModal
       <ProductInfoModal
@@ -23,7 +28,7 @@ const PackageOverview = ({ packageInfo }: { packageInfo: Package }) => {
               {t("networkProvider")}:
               {t("networkProvider")}:
             </div>
             </div>
             <div className="font-bold text-slate-900">
             <div className="font-bold text-slate-900">
-              {packageInfo?.carriers}
+              {formatCarriers(packageInfo?.carriers)}
             </div>
             </div>
           </li>
           </li>
           <li className="flex flex-col sm:flex-row sm:items-baseline gap-1 sm:gap-2">
           <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: {
     server: {
       port: 3000,
       port: 3000,
       host: "0.0.0.0",
       host: "0.0.0.0",
+      allowedHosts: ["e4578e1fbc59.ngrok-free.app"],
     },
     },
     plugins: [react(), tailwindcss()],
     plugins: [react(), tailwindcss()],
     define: {
     define: {