2
0

31 Коміти aa81324485 ... c5491efd6c

Автор SHA1 Опис Дата
  trunghieubui c5491efd6c Add partner login & Lao locale, downgrade to .NET7 8 місяців тому
  trunghieubui 0a9f23359b Update Vietnamese translations and HomeView UI 9 місяців тому
  trunghieubui b21284c8ea Update order views UI and date filter logic 9 місяців тому
  trunghieubui 1725e3a28d Update UI text, images, and data usage calculation 9 місяців тому
  hieubt d92a40c9f7 Update UI text and area search logic 9 місяців тому
  trunghieubui 5a8d642746 Enhance UI, update contact info, and improve FAQ logic 9 місяців тому
  trunghieubui 1e6f5c6aab Merge branch 'master' of https://source.viettech.asia/unitel/ESIM 9 місяців тому
  trunghieubui 771b2d52b4 Refactor order status handling and improve product detail options 9 місяців тому
  hieubt 6e948f735b Merge branch 'master' of https://source.viettech.asia/unitel/ESIM 9 місяців тому
  hieubt 1f6c0f1082 Add Google OAuth login and callback endpoints 9 місяців тому
  trunghieubui d15c75d1f2 Add SkySim assets and update API and frontend configs 9 місяців тому
  hieubt 988f4234c0 Merge remote-tracking branch 'origin/dunt' 9 місяців тому
  hieubt 74e0c58434 Add architecture diagrams and update UI components 9 місяців тому
  trunghieubui 441356b691 Implement localStorage caching for areas and UI fixes 9 місяців тому
  trunghieubui 29298aa28e feat: Implement internationalization in LoginView, ProductDetailView, and SupportView 9 місяців тому
  trunghieubui 8a88ab4f7a Add i18n support and integrate translations 9 місяців тому
  hieubt 053a26d214 Fix price display and hide Physical SIM button 10 місяців тому
  hieubt cb37069497 Add SSL certificates and refactor price formatting 10 місяців тому
  hieubt 1ce1f3b3b9 Fix package state initialization and update effect deps 10 місяців тому
  hieubt 1867dc780f Refactor mutation hook in ProductDetailView 10 місяців тому
  hieubt b229a1565b Refactor package loading to use mutation and add util 10 місяців тому
  hieubt 07086a6645 Split SIM list into countries and regions 10 місяців тому
  trunghieubui 58d83c54c6 Update branding assets and favicon 10 місяців тому
  trunghieubui 6279553265 Enhance area search, authentication, and UI consistency 10 місяців тому
  trunghieubui 98823b8121 Add QR code modal and refactor popup state 10 місяців тому
  trunghieubui b25875e9fa Add order history and compatibility modal features 10 місяців тому
  trunghieubui 8cff814789 Add checkout flow, popup, and dynamic product loading 10 місяців тому
  trunghieubui 181e2e3648 Merge branch 'master' of https://source.viettech.asia/unitel/ESIM 10 місяців тому
  trunghieubui f356e903e2 Add Tailwind CSS and new API modules 10 місяців тому
  ducnt ae5026e07e Merge branch 'master' of https://source.viettech.asia/unitel/ESIM 10 місяців тому
  hieubt eec5c0de28 Initialize Esim Vite React project 10 місяців тому
100 змінених файлів з 13990 додано та 248 видалено
  1. 1 1
      EsimLao/Common/Common.csproj
  2. 7 23
      EsimLao/Common/Constant/CommonConstant.cs
  3. 11 0
      EsimLao/Common/Http/AuthRequest.cs
  4. 1 1
      EsimLao/Database/Database.csproj
  5. 9 1
      EsimLao/Esim.Apis/.config/dotnet-tools.json
  6. 3 3
      EsimLao/Esim.Apis/Business/Content/ContentBusinessImpl.cs
  7. 1 0
      EsimLao/Esim.Apis/Business/User/IUserBusiness.cs
  8. 402 153
      EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs
  9. 18 2
      EsimLao/Esim.Apis/Controllers/UserController.cs
  10. 5 5
      EsimLao/Esim.Apis/Esim.Apis.csproj
  11. 67 49
      EsimLao/Esim.Apis/Program.cs
  12. 2 7
      EsimLao/Esim.Apis/Properties/PublishProfiles/FolderProfile.pubxml
  13. 5 3
      EsimLao/Esim.Apis/appsettings.json
  14. 269 0
      EsimLao/docs/.$skyhub.drawio.bkp
  15. BIN
      EsimLao/docs/language.xlsx
  16. 269 0
      EsimLao/docs/skyhub.drawio
  17. BIN
      EsimLao/docs/ssl/simgetgo_vn.zip
  18. 34 0
      EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_RootCA.crt
  19. 36 0
      EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_cert.crt
  20. 105 0
      EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_cert_inter_root.crt
  21. 18 0
      EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_csr.csr
  22. 1 0
      EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_passwordpfx.txt
  23. 28 0
      EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_private_key.key
  24. 24 0
      EsimLao/esim-vite/.gitignore
  25. 20 0
      EsimLao/esim-vite/README.md
  26. 33 0
      EsimLao/esim-vite/SkySim/SkySim/Logo Skysim.ai
  27. 78 0
      EsimLao/esim-vite/SkySim/SkySim/Logo Skysim.svg
  28. BIN
      EsimLao/esim-vite/SkySim/SkySim/i.png
  29. BIN
      EsimLao/esim-vite/SkySim/SkySim/k.png
  30. BIN
      EsimLao/esim-vite/SkySim/SkySim/logo-skysim.png
  31. BIN
      EsimLao/esim-vite/SkySim/SkySim/m.png
  32. BIN
      EsimLao/esim-vite/SkySim/SkySim/s.png
  33. BIN
      EsimLao/esim-vite/SkySim/SkySim/text.png
  34. BIN
      EsimLao/esim-vite/SkySim/SkySim/y.png
  35. BIN
      EsimLao/esim-vite/dist.zip
  36. BIN
      EsimLao/esim-vite/favicon-48.png
  37. BIN
      EsimLao/esim-vite/favicon-96.png
  38. BIN
      EsimLao/esim-vite/favicon.ico
  39. 80 0
      EsimLao/esim-vite/index.html
  40. 7 0
      EsimLao/esim-vite/metadata.json
  41. 5195 0
      EsimLao/esim-vite/package-lock.json
  42. 34 0
      EsimLao/esim-vite/package.json
  43. 95 0
      EsimLao/esim-vite/src/App.tsx
  44. 37 0
      EsimLao/esim-vite/src/apis/articleApi.ts
  45. 45 0
      EsimLao/esim-vite/src/apis/authApi.ts
  46. 40 0
      EsimLao/esim-vite/src/apis/axios.ts
  47. 61 0
      EsimLao/esim-vite/src/apis/baseApi.ts
  48. 71 0
      EsimLao/esim-vite/src/apis/contentApi.ts
  49. 72 0
      EsimLao/esim-vite/src/apis/productApi.ts
  50. 17 0
      EsimLao/esim-vite/src/app/store.ts
  51. 78 0
      EsimLao/esim-vite/src/assets/img/getgo.svg
  52. 65 0
      EsimLao/esim-vite/src/assets/img/getgo1.svg
  53. 65 0
      EsimLao/esim-vite/src/assets/img/getgo_gradient.svg
  54. BIN
      EsimLao/esim-vite/src/assets/img/logo-skysim.png
  55. BIN
      EsimLao/esim-vite/src/assets/img/logo.png
  56. BIN
      EsimLao/esim-vite/src/assets/img/logo1.png
  57. BIN
      EsimLao/esim-vite/src/assets/img/logo2.png
  58. BIN
      EsimLao/esim-vite/src/assets/img/logo_color.png
  59. BIN
      EsimLao/esim-vite/src/assets/img/logo_white.png
  60. BIN
      EsimLao/esim-vite/src/assets/img/partner1.png
  61. BIN
      EsimLao/esim-vite/src/assets/img/partner2.png
  62. 176 0
      EsimLao/esim-vite/src/components/ChatView.tsx
  63. 254 0
      EsimLao/esim-vite/src/components/CompatibilityModal.tsx
  64. 253 0
      EsimLao/esim-vite/src/components/Footer.tsx
  65. 1054 0
      EsimLao/esim-vite/src/components/Header.tsx
  66. 141 0
      EsimLao/esim-vite/src/components/ImageView.tsx
  67. 107 0
      EsimLao/esim-vite/src/components/Popup.tsx
  68. 50 0
      EsimLao/esim-vite/src/components/ProductCard.tsx
  69. 402 0
      EsimLao/esim-vite/src/components/ProductInfoModal.tsx
  70. 54 0
      EsimLao/esim-vite/src/components/QRCodeModal.tsx
  71. 18 0
      EsimLao/esim-vite/src/components/ScrollToTop.tsx
  72. 121 0
      EsimLao/esim-vite/src/components/SearchView.tsx
  73. 69 0
      EsimLao/esim-vite/src/components/Sidebar.tsx
  74. 37 0
      EsimLao/esim-vite/src/components/TestimonialCard.tsx
  75. 35 0
      EsimLao/esim-vite/src/components/TopLoader.tsx
  76. 191 0
      EsimLao/esim-vite/src/components/VideoView.tsx
  77. 35 0
      EsimLao/esim-vite/src/features/account/accuntSlice.ts
  78. 22 0
      EsimLao/esim-vite/src/features/areas/areasSlice.ts
  79. 37 0
      EsimLao/esim-vite/src/features/loading/loadingSlice.ts
  80. 68 0
      EsimLao/esim-vite/src/features/popup/popupSlice.ts
  81. 21 0
      EsimLao/esim-vite/src/global/constants.ts
  82. 5 0
      EsimLao/esim-vite/src/hooks/useRedux.ts
  83. 20 0
      EsimLao/esim-vite/src/i18n/index.ts
  84. 230 0
      EsimLao/esim-vite/src/i18n/locales/en.json
  85. 231 0
      EsimLao/esim-vite/src/i18n/locales/la.json
  86. 231 0
      EsimLao/esim-vite/src/i18n/locales/vi.json
  87. 20 0
      EsimLao/esim-vite/src/index.css
  88. 24 0
      EsimLao/esim-vite/src/index.tsx
  89. 106 0
      EsimLao/esim-vite/src/logic/loigicUtils.ts
  90. 186 0
      EsimLao/esim-vite/src/pages/buy-sim/BuySimView.tsx
  91. 493 0
      EsimLao/esim-vite/src/pages/checkout/CheckoutView.tsx
  92. 80 0
      EsimLao/esim-vite/src/pages/contact/ContactView.tsx
  93. 484 0
      EsimLao/esim-vite/src/pages/home/HomeView.tsx
  94. 115 0
      EsimLao/esim-vite/src/pages/home/components/HomeBanner.tsx
  95. 130 0
      EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx
  96. 150 0
      EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx
  97. 217 0
      EsimLao/esim-vite/src/pages/home/components/HomeSearch.tsx
  98. 92 0
      EsimLao/esim-vite/src/pages/home/components/HomeTestimonial.tsx
  99. 442 0
      EsimLao/esim-vite/src/pages/login/LoginView.tsx
  100. 180 0
      EsimLao/esim-vite/src/pages/news/NewsDetailView.tsx

+ 1 - 1
EsimLao/Common/Common.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net9.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
 	  

+ 7 - 23
EsimLao/Common/Constant/CommonConstant.cs

@@ -17,7 +17,6 @@ public static class CommonConstant
     public const string NextQuestionFail = "-8";
     public const string OtpWrong = "-9";
 
-
     public const string Unauthorized = "-401";
     public const string BadRequest = "-402";
 
@@ -29,7 +28,6 @@ public static class CommonConstant
     public const string ANSWER_RIGHT = "RIGHT";
     public const string ANSWER_WRONG = "WRONG";
 
-
     public const String PathImage = "/outside";
 
     public const String MissitonTypeLOGIN = "LOGIN";
@@ -37,7 +35,6 @@ public static class CommonConstant
     public const String MissitonTypeREGISTERS = "REGISTERS";
     public const String MissitonTypeINVITING = "INVITING";
 
-
     public const int StatusActive = 1;
     public const int StatusInactive = 0;
     public const int StatusNotClaimed = 2;
@@ -58,13 +55,11 @@ public static class CommonConstant
     public const string TypeInviting = "INVITING";
     public const string TypeRenewService = "RENEW";
 
-
     public const string PathOutside = "PathOutside";
 
     public const string CheckSub = "CHECKSUB";
     public const string Subscribe = "SUBSCRIBE";
 
-
     public static readonly List<int> ListStatusInt = new List<int> { StatusActive, StatusInactive };
     public static readonly List<string> ListCampaignType = new List<string> { "EU" };
     public static readonly List<string> ListMissionType = new List<string>
@@ -83,9 +78,10 @@ public static class CommonConstant
         "REST_POST",
         "REST_GET"
     };
-    public static readonly List<string> ListServiceActionCode = new List<string> { 
+    public static readonly List<string> ListServiceActionCode = new List<string>
+    {
         "REGISTER",
-        "BUY_TURN", 
+        "BUY_TURN",
     };
 
     public static readonly List<string> ListVendorPackagePeriod = new List<string>
@@ -102,18 +98,9 @@ public static class CommonConstant
         "VIP"
     };
 
-    public static readonly List<string> ListMoneyType = new List<string>
-    {
-        "CENT",
-        "USD",
-        "EUR"
-    };
+    public static readonly List<string> ListMoneyType = new List<string> { "CENT", "USD", "EUR" };
 
-    public static readonly List<string> ListPrizeType = new List<string>
-    {
-        "POINT",
-        "MONEY"
-    };
+    public static readonly List<string> ListPrizeType = new List<string> { "POINT", "MONEY" };
 
     public const string DateTimeFormar1 = "dd/MM/yyyy HH:mm:ss";
     public const string DateTimeFormar2 = "MMMM yyyy";
@@ -162,6 +149,7 @@ public static class ApiUrlConstant
     public const String ResendOtpUrl = "/apis/auth/resend-otp";
     public const String GoogleLoginUrl = "/apis/auth/google-login";
     public const String GoogleCallbackUrl = "/apis/auth/google-callback";
+    public const String PartnerLoginUrl = "/apis/auth/partner-login";
 
     // Article URLs
     public const String ArticleCategoryUrl = "/apis/article/category";
@@ -298,9 +286,5 @@ public static class CommonErrorCode
     public const string DuplicateEntry = "-804";
 
     // Prize Types (kept for backward compatibility)
-    public static readonly List<string> ListPrizeType = new List<string>
-    {
-        "POINT",
-        "MONEY"
-    };
+    public static readonly List<string> ListPrizeType = new List<string> { "POINT", "MONEY" };
 }

+ 11 - 0
EsimLao/Common/Http/AuthRequest.cs

@@ -54,4 +54,15 @@ namespace Common.Http
         /// </summary>
         public string? lang { get; set; } = "lo";
     }
+
+    public class PartnerLoginReq
+    {
+        [Required(ErrorMessage = "Authorization code is required")]
+        public string data { get; set; }
+
+        [Required(ErrorMessage = "Partner is required")]
+        public string partner { get; set; } // Optional: if frontend handles redirect, it might need to pass this
+
+    }
+
 }

+ 1 - 1
EsimLao/Database/Database.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net9.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
   </PropertyGroup>

+ 9 - 1
EsimLao/Esim.Apis/.config/dotnet-tools.json

@@ -1,5 +1,13 @@
 {
   "version": 1,
   "isRoot": true,
-  "tools": {}
+  "tools": {
+    "dotnet-ef": {
+      "version": "10.0.2",
+      "commands": [
+        "dotnet-ef"
+      ],
+      "rollForward": false
+    }
+  }
 }

+ 3 - 3
EsimLao/Esim.Apis/Business/Content/ContentBusinessImpl.cs

@@ -266,16 +266,16 @@ namespace Esim.Apis.Business
                 int pageSize = request.pageSize <= 0 ? 10 : request.pageSize;
 
                 var query = dbContext.Faqs
-                    .Where(f => f.Status.HasValue && f.Status.Value);
+                    .Where(f => f.Status == true);
 
                 if (request.categoryId.HasValue)
                 {
                     query = query.Where(f => f.CategoryId == request.categoryId);
                 }
 
-                if (request.isFeatured.HasValue && request.isFeatured.Value)
+                if (request.isFeatured != null)
                 {
-                    query = query.Where(f => f.IsFeatured.HasValue && f.IsFeatured.Value);
+                    query = query.Where(f => f.IsFeatured == request.isFeatured);
                 }
 
                 int totalCount = query.Count();

+ 1 - 0
EsimLao/Esim.Apis/Business/User/IUserBusiness.cs

@@ -19,6 +19,7 @@ namespace Esim.Apis.Business
         Task<IActionResult> RequestOtp(HttpRequest httpRequest, RequestOtpReq request);
         Task<IActionResult> ResendOtp(HttpRequest httpRequest, RequestOtpReq request);
         Task<IActionResult> VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request);
+        Task<IActionResult> PartnerLogin(HttpRequest httpRequest, PartnerLoginReq request);
         Task<IActionResult> GoogleLogin(HttpRequest httpRequest, GoogleLoginReq request);
         Task<IActionResult> GoogleCallback(HttpRequest httpRequest, GoogleCallbackReq request);
     }

+ 402 - 153
EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs

@@ -2,21 +2,22 @@ using Common;
 using Common.Constant;
 using Common.Http;
 using Common.Logic;
-//using Esim.Apis.Logic;
-using Esim.Apis.Singleton;
 using Database;
 using Database.Database;
+//using Esim.Apis.Logic;
+using Esim.Apis.Singleton;
 using log4net;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.VisualBasic;
 using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
 using System;
-using System.Threading.Tasks;
-using System.Xml.Serialization;
+using System.Linq;
 using System.Net.Http;
 using System.Net.Http.Headers;
-using Newtonsoft.Json.Linq;
-using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
 
 namespace Esim.Apis.Business
 {
@@ -40,7 +41,6 @@ namespace Esim.Apis.Business
             return configuration.GetSection(key).Value ?? "";
         }
 
-
         /// <summary>
         /// Request OTP to be sent to email
         /// </summary>
@@ -68,8 +68,8 @@ namespace Esim.Apis.Business
                 string otpCode = isTestAccount ? "111111" : GenerateOtp();
 
                 // Check if customer exists, if not create new
-                var customer = dbContext.CustomerInfos
-                    .Where(c => c.Email == request.email)
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == request.email)
                     .FirstOrDefault();
 
                 decimal? customerId = customer?.Id;
@@ -77,11 +77,14 @@ namespace Esim.Apis.Business
                 if (customer == null)
                 {
                     // Create new customer record - manually get ID from Oracle sequence
-                    var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
-                    
+                    var newCustomerId = await Database.DbLogic.GenIdAsync(
+                        dbContext,
+                        "CUSTOMER_INFO_SEQ"
+                    );
+
                     // Extract name from email (part before @)
                     string emailUsername = request.email.Split('@')[0];
-                    
+
                     var newCustomer = new CustomerInfo
                     {
                         Id = newCustomerId,
@@ -101,48 +104,60 @@ namespace Esim.Apis.Business
                 // Check if there's an existing OTP record for this email (ANY record, used or unused)
                 int otpExpireMinutes = 5;
                 int minSecondsBetweenRequests = 60; // Anti-spam: minimum 60 seconds between requests
-                
-                var existingOtp = dbContext.OtpVerifications
-                    .Where(o => o.UserEmail == request.email)
+
+                var existingOtp = dbContext
+                    .OtpVerifications.Where(o => o.UserEmail == request.email)
                     .OrderByDescending(o => o.CreatedDate)
                     .FirstOrDefault();
 
                 if (existingOtp != null)
                 {
                     // Anti-spam check: prevent rapid OTP requests
-                    var secondsSinceLastRequest = (DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)).TotalSeconds;
+                    var secondsSinceLastRequest = (
+                        DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)
+                    ).TotalSeconds;
                     if (secondsSinceLastRequest < minSecondsBetweenRequests)
                     {
-                        var waitSeconds = (int)(minSecondsBetweenRequests - secondsSinceLastRequest);
-                        log.Warn($"Spam prevention: OTP request too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago.");
-                        
+                        var waitSeconds = (int)(
+                            minSecondsBetweenRequests - secondsSinceLastRequest
+                        );
+                        log.Warn(
+                            $"Spam prevention: OTP request too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago."
+                        );
+
                         return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.OtpTooManyRequests,
                             $"Please wait {waitSeconds} seconds before requesting a new OTP.",
-                            new { 
+                            new
+                            {
                                 waitSeconds = waitSeconds,
-                                canRequestAt = existingOtp.CreatedDate?.AddSeconds(minSecondsBetweenRequests)
+                                canRequestAt = existingOtp.CreatedDate?.AddSeconds(
+                                    minSecondsBetweenRequests
+                                )
                             }
                         );
                     }
-                    
+
                     // UPDATE existing record - reuse for this email to prevent table bloat
                     existingOtp.OtpCode = otpCode;
                     existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
                     existingOtp.AttemptCount = 0; // Reset attempt count
-                    existingOtp.IsUsed = false;   // Reset to unused (allow reuse)
+                    existingOtp.IsUsed = false; // Reset to unused (allow reuse)
                     existingOtp.CustomerId = customerId; // Update customer ID if changed
                     existingOtp.CreatedDate = DateTime.Now; // Update to current time
-                    
-                    log.Info($"Updated existing OTP record ID={existingOtp.Id} for {request.email} (was {(existingOtp.IsUsed == true ? "used" : "unused")})");
+
+                    log.Info(
+                        $"Updated existing OTP record ID={existingOtp.Id} for {request.email} (was {(existingOtp.IsUsed == true ? "used" : "unused")})"
+                    );
                 }
                 else
                 {
                     // INSERT new record only for first-time email
-                    var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
+                    var otpId = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
                     var otpVerification = new OtpVerification
                     {
                         Id = otpId,
@@ -171,8 +186,9 @@ namespace Esim.Apis.Business
                     string templateCode = "OTP_LOGIN";
 
                     // Query template and get language-specific content
-                    var template = dbContext.MessageTemplates
-                        .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value);
+                    var template = dbContext.MessageTemplates.FirstOrDefault(
+                        t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value
+                    );
 
                     if (template == null)
                     {
@@ -181,14 +197,20 @@ namespace Esim.Apis.Business
                     }
 
                     // Get subject based on language: vi=default, en=_EN, lo=_LO (default)
-                    string emailSubject = lang == "en" ? (template.SubjectEn ?? template.Subject ?? "")
-                                        : lang == "vi" ? (template.Subject ?? "")
-                                        : (template.SubjectLo ?? template.Subject ?? "");
+                    string emailSubject =
+                        lang == "en"
+                            ? (template.SubjectEn ?? template.Subject ?? "")
+                            : lang == "vi"
+                                ? (template.Subject ?? "")
+                                : (template.SubjectLo ?? template.Subject ?? "");
 
                     // Get content based on language: vi=default, en=_EN, lo=_LO (default)
-                    string emailContent = lang == "en" ? (template.ContentEn ?? template.Content ?? "")
-                                        : lang == "vi" ? (template.Content ?? "")
-                                        : (template.ContentLo ?? template.Content ?? "");
+                    string emailContent =
+                        lang == "en"
+                            ? (template.ContentEn ?? template.Content ?? "")
+                            : lang == "vi"
+                                ? (template.Content ?? "")
+                                : (template.ContentLo ?? template.Content ?? "");
 
                     // Replace placeholders in content
                     emailContent = emailContent
@@ -199,14 +221,15 @@ namespace Esim.Apis.Business
                     emailSubject = emailSubject
                         .Replace("{{OTP_CODE}}", otpCode)
                         .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
-                    var emailMessageID = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
+                    var emailMessageID = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
                     var emailMessage = new MessageQueue
                     {
                         Id = emailMessageID,
                         MessageType = 1, // Email
                         Recipient = request.email,
-                        Subject = emailSubject,     // Pre-resolved subject
-                        Content = emailContent,     // Pre-resolved content
+                        Subject = emailSubject, // Pre-resolved subject
+                        Content = emailContent, // Pre-resolved content
                         Priority = true, // High priority
                         Status = 0, // Pending
                         ScheduledAt = DateTime.Now,
@@ -220,7 +243,9 @@ namespace Esim.Apis.Business
                     await dbContext.SaveChangesAsync();
                 }
 
-                log.Info($"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
+                log.Info(
+                    $"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}"
+                );
 
                 //return DotnetLib.Http.HttpResponse.BuildResponse(
                 //    log,
@@ -241,11 +266,7 @@ namespace Esim.Apis.Business
                     json,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
-                    new
-                    {
-                        email = request.email,
-                        expireInSeconds = otpExpireMinutes * 60
-                    }
+                    new { email = request.email, expireInSeconds = otpExpireMinutes * 60 }
                 );
             }
             catch (Exception exception)
@@ -271,11 +292,11 @@ namespace Esim.Apis.Business
             var url = httpRequest.Path;
             var json = JsonConvert.SerializeObject(request);
             log.Debug("URL: " + url + " => ResendOtp Request: " + json);
-            
+
             try
             {
                 string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
-                
+
                 // Validate email is required
                 if (string.IsNullOrEmpty(request.email))
                 {
@@ -290,8 +311,8 @@ namespace Esim.Apis.Business
                 }
 
                 // Check if there's an existing OTP record for this email
-                var existingOtp = dbContext.OtpVerifications
-                    .Where(o => o.UserEmail == request.email)
+                var existingOtp = dbContext
+                    .OtpVerifications.Where(o => o.UserEmail == request.email)
                     .OrderByDescending(o => o.CreatedDate)
                     .FirstOrDefault();
 
@@ -311,22 +332,29 @@ namespace Esim.Apis.Business
 
                 // RESEND has reduced cooldown: 60 seconds (vs 60 seconds for RequestOtp)
                 int minSecondsBetweenResend = 60;
-                var secondsSinceLastRequest = (DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)).TotalSeconds;
-                
+                var secondsSinceLastRequest = (
+                    DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)
+                ).TotalSeconds;
+
                 if (secondsSinceLastRequest < minSecondsBetweenResend)
                 {
                     var waitSeconds = (int)(minSecondsBetweenResend - secondsSinceLastRequest);
-                    log.Warn($"ResendOtp: Too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago.");
-                    
+                    log.Warn(
+                        $"ResendOtp: Too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago."
+                    );
+
                     return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         url,
                         json,
                         CommonErrorCode.OtpTooManyRequests,
                         $"Please wait {waitSeconds} seconds before resending OTP.",
-                        new { 
+                        new
+                        {
                             waitSeconds = waitSeconds,
-                            canResendAt = existingOtp.CreatedDate?.AddSeconds(minSecondsBetweenResend)
+                            canResendAt = existingOtp.CreatedDate?.AddSeconds(
+                                minSecondsBetweenResend
+                            )
                         }
                     );
                 }
@@ -334,13 +362,13 @@ namespace Esim.Apis.Business
                 // Generate new 6-digit OTP (fixed 111111 for test account abc@gmail.com)
                 bool isTestAccount = request.email.ToLower() == "abc@gmail.com";
                 string otpCode = isTestAccount ? "111111" : GenerateOtp();
-                
+
                 // OTP expires in 5 minutes
                 int otpExpireMinutes = 5;
 
                 // Get customer ID (should exist since OTP record exists)
-                var customer = dbContext.CustomerInfos
-                    .Where(c => c.Email == request.email)
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == request.email)
                     .FirstOrDefault();
                 decimal? customerId = customer?.Id ?? existingOtp.CustomerId;
 
@@ -348,12 +376,12 @@ namespace Esim.Apis.Business
                 existingOtp.OtpCode = otpCode;
                 existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
                 existingOtp.AttemptCount = 0; // Reset attempt count
-                existingOtp.IsUsed = false;   // Reset to unused
+                existingOtp.IsUsed = false; // Reset to unused
                 existingOtp.CustomerId = customerId;
                 existingOtp.CreatedDate = DateTime.Now; // Update to track resend time
-                
+
                 log.Info($"ResendOtp: Updated OTP record ID={existingOtp.Id} for {request.email}");
-                
+
                 await dbContext.SaveChangesAsync();
 
                 // Skip email sending for test account
@@ -361,9 +389,10 @@ namespace Esim.Apis.Business
                 {
                     // Add to MESSAGE_QUEUE for background email sending
                     string templateCode = "OTP_LOGIN";
-                    
-                    var template = dbContext.MessageTemplates
-                        .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value);
+
+                    var template = dbContext.MessageTemplates.FirstOrDefault(
+                        t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value
+                    );
 
                     if (template == null)
                     {
@@ -372,14 +401,20 @@ namespace Esim.Apis.Business
                     }
 
                     // Get subject based on language
-                    string emailSubject = lang == "en" ? (template.SubjectEn ?? template.Subject ?? "")
-                                        : lang == "vi" ? (template.Subject ?? "")
-                                        : (template.SubjectLo ?? template.Subject ?? "");
+                    string emailSubject =
+                        lang == "en"
+                            ? (template.SubjectEn ?? template.Subject ?? "")
+                            : lang == "vi"
+                                ? (template.Subject ?? "")
+                                : (template.SubjectLo ?? template.Subject ?? "");
 
                     // Get content based on language
-                    string emailContent = lang == "en" ? (template.ContentEn ?? template.Content ?? "")
-                                        : lang == "vi" ? (template.Content ?? "")
-                                        : (template.ContentLo ?? template.Content ?? "");
+                    string emailContent =
+                        lang == "en"
+                            ? (template.ContentEn ?? template.Content ?? "")
+                            : lang == "vi"
+                                ? (template.Content ?? "")
+                                : (template.ContentLo ?? template.Content ?? "");
 
                     // Replace placeholders
                     emailContent = emailContent
@@ -390,7 +425,8 @@ namespace Esim.Apis.Business
                         .Replace("{{OTP_CODE}}", otpCode)
                         .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
 
-                    var emailMessageID = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
+                    var emailMessageID = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
                     var emailMessage = new MessageQueue
                     {
                         Id = emailMessageID,
@@ -411,7 +447,9 @@ namespace Esim.Apis.Business
                     await dbContext.SaveChangesAsync();
                 }
 
-                log.Info($"ResendOtp: OTP resent for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
+                log.Info(
+                    $"ResendOtp: OTP resent for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}"
+                );
 
                 return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
@@ -419,11 +457,7 @@ namespace Esim.Apis.Business
                     json,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("OTP_RESENT_SUCCESS", lang),
-                    new
-                    {
-                        email = request.email,
-                        expireInSeconds = otpExpireMinutes * 60
-                    }
+                    new { email = request.email, expireInSeconds = otpExpireMinutes * 60 }
                 );
             }
             catch (Exception exception)
@@ -467,19 +501,24 @@ namespace Esim.Apis.Business
                 string responseLang = CommonLogic.GetLanguage(httpRequest, request.lang);
 
                 // Find valid OTP
-                var otpRecord = dbContext.OtpVerifications
-                    .Where(o => o.UserEmail == request.email
-                        && o.OtpCode == request.otpCode
-                        && o.IsUsed == false
-                        && o.ExpiredAt > DateTime.Now)
+                var otpRecord = dbContext
+                    .OtpVerifications.Where(
+                        o =>
+                            o.UserEmail == request.email
+                            && o.OtpCode == request.otpCode
+                            && o.IsUsed == false
+                            && o.ExpiredAt > DateTime.Now
+                    )
                     .OrderByDescending(o => o.CreatedDate)
                     .FirstOrDefault();
 
                 if (otpRecord == null)
                 {
                     // Check if OTP exists but expired or used
-                    var anyOtp = dbContext.OtpVerifications
-                        .Where(o => o.UserEmail == request.email && o.OtpCode == request.otpCode)
+                    var anyOtp = dbContext
+                        .OtpVerifications.Where(
+                            o => o.UserEmail == request.email && o.OtpCode == request.otpCode
+                        )
                         .FirstOrDefault();
 
                     if (anyOtp != null)
@@ -491,7 +530,10 @@ namespace Esim.Apis.Business
                                 url,
                                 json,
                                 CommonErrorCode.OtpAlreadyUsed,
-                                ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang),
+                                ConfigManager.Instance.GetConfigWebValue(
+                                    "OTP_ALREADY_USED",
+                                    responseLang
+                                ),
                                 new { }
                             );
                         }
@@ -502,7 +544,10 @@ namespace Esim.Apis.Business
                                 url,
                                 json,
                                 CommonErrorCode.OtpExpired,
-                                ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang),
+                                ConfigManager.Instance.GetConfigWebValue(
+                                    "OTP_EXPIRED",
+                                    responseLang
+                                ),
                                 new { }
                             );
                         }
@@ -522,8 +567,8 @@ namespace Esim.Apis.Business
                 otpRecord.IsUsed = true;
 
                 // Get customer info
-                var customer = dbContext.CustomerInfos
-                    .Where(c => c.Email == request.email)
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == request.email)
                     .FirstOrDefault();
 
                 if (customer == null)
@@ -547,14 +592,21 @@ namespace Esim.Apis.Business
                 int tokenExpireHours = 24;
                 int refreshTokenExpireDays = 30;
 
-                string accessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
-                string refreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
+                string accessToken = CommonLogic.GenToken(
+                    configuration,
+                    customer.Email ?? "",
+                    customer.Id.ToString() ?? ""
+                );
+                string refreshToken = CommonLogic.GenRefreshToken(
+                    configuration,
+                    customer.Email ?? ""
+                );
                 var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
                 var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
 
                 // Revoke old tokens
-                var oldTokens = dbContext.UserTokens
-                    .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
+                var oldTokens = dbContext
+                    .UserTokens.Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
                     .ToList();
 
                 foreach (var oldToken in oldTokens)
@@ -595,6 +647,7 @@ namespace Esim.Apis.Business
                         email = customer.Email ?? "",
                         fullName = $"{customer.SurName} {customer.LastName}".Trim(),
                         avatarUrl = customer.AvatarUrl,
+                        msisdn = customer.PhoneNumber,
                         accessToken,
                         refreshToken,
                         expiresAt
@@ -642,23 +695,26 @@ namespace Esim.Apis.Business
             return ipAddress ?? "Unknown";
         }
 
-        public async Task<IActionResult> GoogleLogin(HttpRequest httpRequest, GoogleLoginReq request)
+        public async Task<IActionResult> GoogleLogin(
+            HttpRequest httpRequest,
+            GoogleLoginReq request
+        )
         {
             var url = httpRequest.Path;
             var json = JsonConvert.SerializeObject(request);
             log.Debug("URL: " + url + " => GoogleLogin Request: " + json);
-            
-            try 
+
+            try
             {
                 string lang = CommonLogic.GetLanguage(httpRequest, request?.lang);
-                
+
                 var clientId = configuration["Google:ClientId"];
                 var redirectUri = configuration["Google:RedirectUri"];
-                
+
                 if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(redirectUri))
                 {
                     log.Error("Google Auth configuration missing");
-                     return DotnetLib.Http.HttpResponse.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         url,
                         json,
@@ -668,21 +724,22 @@ namespace Esim.Apis.Business
                     );
                 }
 
-                var googleUrl = $"https://accounts.google.com/o/oauth2/v2/auth?client_id={clientId}&redirect_uri={redirectUri}&response_type=code&scope=email%20profile";
-                
+                var googleUrl =
+                    $"https://accounts.google.com/o/oauth2/v2/auth?client_id={clientId}&redirect_uri={redirectUri}&response_type=code&scope=email%20profile&prompt=select_account";
+
                 return DotnetLib.Http.HttpResponse.BuildResponse(
-                        log,
-                        url,
-                        json,
-                        CommonErrorCode.Success,
-                        ConfigManager.Instance.GetConfigWebValue("SUCCESS", lang),
-                        new { url = googleUrl }
-                    );
+                    log,
+                    url,
+                    json,
+                    CommonErrorCode.Success,
+                    ConfigManager.Instance.GetConfigWebValue("SUCCESS", lang),
+                    new { url = googleUrl }
+                );
             }
             catch (Exception ex)
             {
-                 log.Error("GoogleLogin Exception: ", ex);
-                 return DotnetLib.Http.HttpResponse.BuildResponse(
+                log.Error("GoogleLogin Exception: ", ex);
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     url,
                     json,
@@ -693,20 +750,23 @@ namespace Esim.Apis.Business
             }
         }
 
-        public async Task<IActionResult> GoogleCallback(HttpRequest httpRequest, GoogleCallbackReq request)
+        public async Task<IActionResult> GoogleCallback(
+            HttpRequest httpRequest,
+            GoogleCallbackReq request
+        )
         {
             var url = httpRequest.Path;
             var json = JsonConvert.SerializeObject(request);
             log.Debug("URL: " + url + " => GoogleCallback Request: " + json);
-            
+
             try
             {
                 // Get language for response messages
                 string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
-                
+
                 if (string.IsNullOrEmpty(request.code))
                 {
-                     return DotnetLib.Http.HttpResponse.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         url,
                         json,
@@ -718,32 +778,42 @@ namespace Esim.Apis.Business
 
                 var clientId = configuration["Google:ClientId"];
                 var clientSecret = configuration["Google:ClientSecret"];
-                var redirectUri = !string.IsNullOrEmpty(request.redirectUri) ? request.redirectUri : configuration["Google:RedirectUri"];
+                var redirectUri = !string.IsNullOrEmpty(request.redirectUri)
+                    ? request.redirectUri
+                    : configuration["Google:RedirectUri"];
 
                 using (var httpClient = new HttpClient())
                 {
                     // 1. Exchange code for token
-                    var tokenRequestContent = new FormUrlEncodedContent(new[]
-                    {
-                        new KeyValuePair<string, string>("code", request.code),
-                        new KeyValuePair<string, string>("client_id", clientId),
-                        new KeyValuePair<string, string>("client_secret", clientSecret),
-                        new KeyValuePair<string, string>("redirect_uri", redirectUri),
-                        new KeyValuePair<string, string>("grant_type", "authorization_code")
-                    });
-
-                    var tokenResponse = await httpClient.PostAsync("https://oauth2.googleapis.com/token", tokenRequestContent);
+                    var tokenRequestContent = new FormUrlEncodedContent(
+                        new[]
+                        {
+                            new KeyValuePair<string, string>("code", request.code),
+                            new KeyValuePair<string, string>("client_id", clientId),
+                            new KeyValuePair<string, string>("client_secret", clientSecret),
+                            new KeyValuePair<string, string>("redirect_uri", redirectUri),
+                            new KeyValuePair<string, string>("grant_type", "authorization_code")
+                        }
+                    );
+
+                    var tokenResponse = await httpClient.PostAsync(
+                        "https://oauth2.googleapis.com/token",
+                        tokenRequestContent
+                    );
                     var tokenResponseString = await tokenResponse.Content.ReadAsStringAsync();
-                    
+
                     if (!tokenResponse.IsSuccessStatusCode)
                     {
-                         log.Error($"Google Token Exchange Failed: {tokenResponseString}");
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log.Error($"Google Token Exchange Failed: {tokenResponseString}");
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.ExternalServiceError,
-                            ConfigManager.Instance.GetConfigWebValue("GOOGLE_TOKEN_EXCHANGE_FAILED", lang),
+                            ConfigManager.Instance.GetConfigWebValue(
+                                "GOOGLE_TOKEN_EXCHANGE_FAILED",
+                                lang
+                            ),
                             new { error = tokenResponseString }
                         );
                     }
@@ -753,30 +823,41 @@ namespace Esim.Apis.Business
 
                     if (string.IsNullOrEmpty(accessToken))
                     {
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.ExternalServiceError,
-                            ConfigManager.Instance.GetConfigWebValue("GOOGLE_NO_ACCESS_TOKEN", lang),
+                            ConfigManager.Instance.GetConfigWebValue(
+                                "GOOGLE_NO_ACCESS_TOKEN",
+                                lang
+                            ),
                             new { }
                         );
                     }
 
                     // 2. Get User Info
-                    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
-                    var userInfoResponse = await httpClient.GetAsync("https://www.googleapis.com/oauth2/v2/userinfo");
+                    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
+                        "Bearer",
+                        accessToken
+                    );
+                    var userInfoResponse = await httpClient.GetAsync(
+                        "https://www.googleapis.com/oauth2/v2/userinfo"
+                    );
                     var userInfoString = await userInfoResponse.Content.ReadAsStringAsync();
 
                     if (!userInfoResponse.IsSuccessStatusCode)
                     {
-                         log.Error($"Google User Info Failed: {userInfoString}");
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log.Error($"Google User Info Failed: {userInfoString}");
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.ExternalServiceError,
-                            ConfigManager.Instance.GetConfigWebValue("GOOGLE_USERINFO_FAILED", lang),
+                            ConfigManager.Instance.GetConfigWebValue(
+                                "GOOGLE_USERINFO_FAILED",
+                                lang
+                            ),
                             new { error = userInfoString }
                         );
                     }
@@ -785,10 +866,10 @@ namespace Esim.Apis.Business
                     var email = userInfo["email"]?.ToString();
                     var name = userInfo["name"]?.ToString(); // Full name
                     var picture = userInfo["picture"]?.ToString();
-                    
+
                     if (string.IsNullOrEmpty(email))
                     {
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
@@ -799,14 +880,16 @@ namespace Esim.Apis.Business
                     }
 
                     // 3. Login or Register logic
-                     var customer = dbContext.CustomerInfos
-                        .FirstOrDefault(c => c.Email == email);
+                    var customer = dbContext.CustomerInfos.FirstOrDefault(c => c.Email == email);
 
                     if (customer == null)
                     {
                         // Register new user
-                        var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
-                        
+                        var newCustomerId = await Database.DbLogic.GenIdAsync(
+                            dbContext,
+                            "CUSTOMER_INFO_SEQ"
+                        );
+
                         // Try to split name
                         string surname = name;
                         string lastname = "";
@@ -819,9 +902,9 @@ namespace Esim.Apis.Business
                                 surname = string.Join(" ", parts.Take(parts.Length - 1));
                             }
                         }
-                        else 
+                        else
                         {
-                             surname = email.Split('@')[0];
+                            surname = email.Split('@')[0];
                         }
 
                         customer = new CustomerInfo
@@ -830,7 +913,7 @@ namespace Esim.Apis.Business
                             Email = email,
                             SurName = surname,
                             LastName = lastname,
-                            AvatarUrl = picture, 
+                            AvatarUrl = picture,
                             Status = true,
                             IsVerified = true, // Verified by Google
                             CreatedDate = DateTime.Now,
@@ -838,34 +921,48 @@ namespace Esim.Apis.Business
                         };
                         dbContext.CustomerInfos.Add(customer);
                         await dbContext.SaveChangesAsync();
-                        log.Info($"Created new customer via Google Login: ID={newCustomerId}, Email={email}");
+                        log.Info(
+                            $"Created new customer via Google Login: ID={newCustomerId}, Email={email}"
+                        );
                     }
-                    else 
+                    else
                     {
                         // Update existing user info if needed or just log them in
-                        customer.IsVerified = true; 
-                        if (string.IsNullOrEmpty(customer.AvatarUrl) && !string.IsNullOrEmpty(picture))
+                        customer.IsVerified = true;
+                        if (
+                            string.IsNullOrEmpty(customer.AvatarUrl)
+                            && !string.IsNullOrEmpty(picture)
+                        )
                         {
                             customer.AvatarUrl = picture;
                         }
                         customer.LastLoginDate = DateTime.Now;
                         customer.LastUpdate = DateTime.Now;
                         await dbContext.SaveChangesAsync();
-                        log.Info($"Existing customer logged in via Google: ID={customer.Id}, Email={email}");
+                        log.Info(
+                            $"Existing customer logged in via Google: ID={customer.Id}, Email={email}"
+                        );
                     }
 
                     // 4. Generate JWT
                     int tokenExpireHours = 24;
                     int refreshTokenExpireDays = 30;
 
-                    string jwtAccessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
-                    string jwtRefreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
+                    string jwtAccessToken = CommonLogic.GenToken(
+                        configuration,
+                        customer.Email ?? "",
+                        customer.Id.ToString() ?? ""
+                    );
+                    string jwtRefreshToken = CommonLogic.GenRefreshToken(
+                        configuration,
+                        customer.Email ?? ""
+                    );
                     var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
                     var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
 
                     // Revoke old tokens
-                    var oldTokens = dbContext.UserTokens
-                        .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
+                    var oldTokens = dbContext
+                        .UserTokens.Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
                         .ToList();
 
                     foreach (var oldToken in oldTokens)
@@ -874,7 +971,8 @@ namespace Esim.Apis.Business
                     }
 
                     // Save new token
-                    var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
+                    var tokenId = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
                     var userToken = new UserToken
                     {
                         Id = tokenId,
@@ -906,6 +1004,7 @@ namespace Esim.Apis.Business
                             email = customer.Email ?? "",
                             fullName = $"{customer.SurName} {customer.LastName}".Trim(),
                             avatarUrl = customer.AvatarUrl,
+                            msisdn = customer.PhoneNumber,
                             accessToken = jwtAccessToken,
                             refreshToken = jwtRefreshToken,
                             expiresAt
@@ -916,7 +1015,7 @@ namespace Esim.Apis.Business
             catch (Exception exception)
             {
                 log.Error("GoogleCallback Exception: ", exception);
-                 return DotnetLib.Http.HttpResponse.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     url,
                     json,
@@ -927,5 +1026,155 @@ namespace Esim.Apis.Business
             }
         }
 
+        public async Task<IActionResult> PartnerLogin(HttpRequest httpRequest, PartnerLoginReq request)
+        {
+            var url = httpRequest.Path;
+            var json = JsonConvert.SerializeObject(request);
+            log.Debug("URL: " + url + " => GoogleCallback Request: " + json);
+
+            try
+            {
+                byte[] keyBytes = Encoding.UTF8.GetBytes(configuration["Partner:EncryptionKey"] ?? "");
+
+                var decrypt = DotnetLib.Secure.SecureLogic.DecryptAes(
+                    request.data.Replace(" ", "+"),
+                    configuration["Partner:EncryptionKey"] ?? ""
+                );
+                if (decrypt == null)
+                {
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log,
+                        url,
+                        json,
+                        CommonErrorCode.Error,
+                        ConfigManager.Instance.GetConfigWebValue("INVALID_PARTNER_DATA"),
+                        new { }
+                    );
+                }
+
+                var requestData = JsonConvert.DeserializeObject<JObject>(decrypt);
+                log.Debug("Decrypted PartnerLogin data: " + JsonConvert.SerializeObject(requestData));
+                string email = requestData["email"]?.ToString() ?? "";
+                if (string.IsNullOrEmpty(email))
+                {
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log,
+                        url,
+                        json,
+                        CommonErrorCode.RequiredFieldMissing,
+                        ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED"),
+                        new { }
+                    );
+                }
+
+                // Get customer info
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == email)
+                    .FirstOrDefault();
+
+                if (customer == null)
+                {
+                    // create a new customer
+                    customer = new CustomerInfo
+                    {
+                        Id = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ"),
+                        Email = email,
+                        SurName = requestData["fullName"]?.ToString() ?? "",
+                        LastName = requestData["fullName"]?.ToString() ?? "",
+                        AvatarUrl = requestData["avatarUrl"]?.ToString() ?? "",
+                        PhoneNumber = requestData["phoneNumber"]?.ToString() ?? "",
+                        Status = true,
+                        IsVerified = true,
+                        CreatedDate = DateTime.Now,
+                        LastUpdate = DateTime.Now
+                    };
+                    dbContext.CustomerInfos.Add(customer);
+                    await dbContext.SaveChangesAsync();
+                }
+
+                // Update customer verification status
+                customer.IsVerified = true;
+                customer.LastLoginDate = DateTime.Now;
+                customer.LastUpdate = DateTime.Now;
+
+                // Generate JWT tokens
+                int tokenExpireHours = 24;
+                int refreshTokenExpireDays = 30;
+
+                string accessToken = CommonLogic.GenToken(
+                    configuration,
+                    customer.Email ?? "",
+                    customer.Id.ToString() ?? ""
+                );
+                string refreshToken = CommonLogic.GenRefreshToken(
+                    configuration,
+                    customer.Email ?? ""
+                );
+                var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
+                var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
+
+                // Revoke old tokens
+                var oldTokens = dbContext
+                    .UserTokens.Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
+                    .ToList();
+
+                foreach (var oldToken in oldTokens)
+                {
+                    oldToken.IsRevoked = true;
+                }
+
+                // Save new token
+                var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
+                var userToken = new UserToken
+                {
+                    Id = tokenId,
+                    CustomerId = customer.Id,
+                    AccessToken = accessToken,
+                    RefreshToken = refreshToken,
+                    TokenType = "Bearer",
+                    DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
+                    IpAddress = GetClientIpAddress(httpRequest),
+                    ExpiredAt = expiresAt,
+                    RefreshExpiredAt = refreshExpiresAt,
+                    IsRevoked = false,
+                    CreatedDate = DateTime.Now,
+                    LastUsed = DateTime.Now
+                };
+
+                dbContext.UserTokens.Add(userToken);
+                await dbContext.SaveChangesAsync();
+
+                return DotnetLib.Http.HttpResponse.BuildResponse(
+                    log,
+                    url,
+                    json,
+                    CommonErrorCode.Success,
+                    ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", "en"),
+                    new
+                    {
+                        userId = customer.Id,
+                        email = customer.Email ?? "",
+                        fullName = $"{customer.SurName} {customer.LastName}".Trim(),
+                        avatarUrl = customer.AvatarUrl,
+                        msisdn = customer.PhoneNumber,
+                        accessToken,
+                        refreshToken,
+                        expiresAt
+                    }
+                );
+            }
+            catch (Exception exception)
+            {
+                log.Error("GoogleCallback Exception: ", exception);
+            }
+            return DotnetLib.Http.HttpResponse.BuildResponse(
+                log,
+                url,
+                json,
+                CommonErrorCode.SystemError,
+                ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
+                new { }
+            );
+        }
     }
 }

+ 18 - 2
EsimLao/Esim.Apis/Controllers/UserController.cs

@@ -2,8 +2,8 @@ using System;
 using Common;
 using Common.Constant;
 using Common.Http;
-using Esim.Apis.Business;
 using Database.Database;
+using Esim.Apis.Business;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 
@@ -81,7 +81,10 @@ namespace RevoSystem.Apis.Controllers
         [Route(ApiUrlConstant.GoogleLoginUrl)]
         public async Task<IActionResult> GoogleLogin([FromBody] GoogleLoginReq request)
         {
-            return await userBusiness.GoogleLogin(HttpContext.Request, request ?? new GoogleLoginReq());
+            return await userBusiness.GoogleLogin(
+                HttpContext.Request,
+                request ?? new GoogleLoginReq()
+            );
         }
 
         /// <summary>
@@ -98,5 +101,18 @@ namespace RevoSystem.Apis.Controllers
             return await userBusiness.GoogleCallback(HttpContext.Request, request);
         }
 
+        /// <summary>
+        /// Google OAuth Callback (Exchange code for token)
+        /// POST /apis/auth/google-callback
+        /// </summary>
+        /// <param name="request">Contains the authorization 'code' (required) and 'redirectUri' (optional)</param>
+        /// <returns>Returns user profile and JWT tokens (accessToken, refreshToken)</returns>
+        [ProducesResponseType(typeof(object), 200)]
+        [HttpPost]
+        [Route(ApiUrlConstant.PartnerLoginUrl)]
+        public async Task<IActionResult> PartnerLogin([FromBody] PartnerLoginReq request)
+        {
+            return await userBusiness.PartnerLogin(HttpContext.Request, request);
+        }
     }
 }

+ 5 - 5
EsimLao/Esim.Apis/Esim.Apis.csproj

@@ -1,14 +1,14 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
-    <TargetFramework>net9.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
   </PropertyGroup>
 	<PropertyGroup Condition=" '$(RunConfiguration)' == 'https' " />
 	<PropertyGroup Condition=" '$(RunConfiguration)' == 'http' " />
 	<ItemGroup>
-		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
+		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
 		<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" />
 		<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
 		<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
@@ -23,12 +23,12 @@
 		<PackageReference Include="NCrontab" Version="3.3.3" />
 	</ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="..\Common\Common.csproj" />
-    <ProjectReference Include="..\Database\Database.csproj" />
+    <ProjectReference Include="../Common/Common.csproj" />
+    <ProjectReference Include="../Database/Database.csproj" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="DotnetLib">
-      <HintPath>..\..\..\..\lib\DotnetLib.dll</HintPath>
+      <HintPath>..\lib\DotnetLib.dll</HintPath>
     </Reference>
   </ItemGroup>
 

+ 67 - 49
EsimLao/Esim.Apis/Program.cs

@@ -1,34 +1,40 @@
-using System.Text;
 using Common.Global;
 using Database.Database;
 using Esim.Apis.Business;
+using log4net;
+using log4net.Config;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.IdentityModel.Tokens;
-
-using log4net;
-using log4net.Config;
+using System.Security.Cryptography;
+using System.Text;
 
 var builder = WebApplication.CreateBuilder(args);
 
 // Initialize log4net
-XmlConfigurator.Configure(new FileInfo(Path.Combine(builder.Environment.ContentRootPath, "log4net.config")));
+XmlConfigurator.Configure(
+    new FileInfo(Path.Combine(builder.Environment.ContentRootPath, "log4net.config"))
+);
 
 // Set global configuration for use by singletons like ConfigManager
 GlobalConfig.Configuration = builder.Configuration;
 
 // Add services to the container.
-builder.Services.AddControllersWithViews()
+builder
+    .Services.AddControllersWithViews()
     .AddJsonOptions(options =>
     {
         // Use camelCase for JSON property names (categoryId instead of CategoryId)
-        options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
+        options.JsonSerializerOptions.PropertyNamingPolicy = System
+            .Text
+            .Json
+            .JsonNamingPolicy
+            .CamelCase;
     });
 
 // Add DbContext with Oracle provider
 var connectionString = builder.Configuration.GetSection("Connection").Value;
-builder.Services.AddDbContext<ModelContext>(options =>
-    options.UseOracle(connectionString));
+builder.Services.AddDbContext<ModelContext>(options => options.UseOracle(connectionString));
 
 // Add Business Services
 builder.Services.AddScoped<IUserBusiness, UserBusinessImpl>();
@@ -38,46 +44,55 @@ builder.Services.AddScoped<IContentBusiness, ContentBusinessImpl>();
 // Configure CORS - Allow frontend to access APIs
 builder.Services.AddCors(options =>
 {
-    options.AddPolicy("AllowFrontend", policy =>
-    {
-        policy.WithOrigins(
-                "http://localhost:3000",      // React development
-                "http://localhost:5173",      // Vite development
-                "http://localhost:4200",      // Angular development
-                "http://simgetgo.vn",        // Production domain
-                "http://simgetgo.com",     // Production www domain
-                "https://simgetgo.vn",    // Production www domain
-                "https://simgetgo.com"     // Production www domain
-            )
-            .AllowAnyMethod()
-            .AllowAnyHeader()
-            .AllowCredentials();
-    });
+    options.AddPolicy(
+        "AllowFrontend",
+        policy =>
+        {
+            policy
+                .WithOrigins(
+                    "http://localhost:3000", // React development
+                    "http://localhost:5173", // Vite development
+                    "http://localhost:4200", // Angular development
+                    "http://simgetgo.vn", // Production domain
+                    "http://simgetgo.com", // Production www domain
+                    "https://skysimhub.vn", // Production www domain
+                    "https://skysimhub.com", // Production www domain
+                    "https://simgetgo.vn", // Production www domain
+                    "https://simgetgo.com" // Production www domain
+                )
+                .AllowAnyMethod()
+                .AllowAnyHeader()
+                .AllowCredentials();
+        }
+    );
 });
 
 // Configure JWT Authentication
-var jwtKey = builder.Configuration["Jwt:Key"] ?? "EsimLaoSecretKey1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD";
+var jwtKey =
+    builder.Configuration["Jwt:Key"]
+    ?? "EsimLaoSecretKey1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD";
 var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "EsimLao";
 var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "EsimLaoClient";
 
-builder.Services.AddAuthentication(options =>
-{
-    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
-    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
-})
-.AddJwtBearer(options =>
-{
-    options.TokenValidationParameters = new TokenValidationParameters
+builder
+    .Services.AddAuthentication(options =>
     {
-        ValidateIssuer = true,
-        ValidateAudience = true,
-        ValidateLifetime = true,
-        ValidateIssuerSigningKey = true,
-        ValidIssuer = jwtIssuer,
-        ValidAudience = jwtAudience,
-        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtKey))
-    };
-});
+        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+    })
+    .AddJwtBearer(options =>
+    {
+        options.TokenValidationParameters = new TokenValidationParameters
+        {
+            ValidateIssuer = true,
+            ValidateAudience = true,
+            ValidateLifetime = true,
+            ValidateIssuerSigningKey = true,
+            ValidIssuer = jwtIssuer,
+            ValidAudience = jwtAudience,
+            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtKey))
+        };
+    });
 
 // Add Swagger for API documentation
 builder.Services.AddEndpointsApiExplorer();
@@ -94,7 +109,12 @@ using (var scope = app.Services.CreateScope())
         logger.LogInformation("Initializing ConfigManager...");
         Esim.Apis.Singleton.ConfigManager.Instance.Initialize();
         logger.LogInformation("ConfigManager initialized successfully");
-        
+        byte[] keyBytes = Encoding.UTF8.GetBytes("9kw1wLMLG2u1j2zX");
+        var encrypt = DotnetLib.Secure.SecureLogic.EncryptAes("{\"username\": \"10014331\",\"fullName\": \"User6066871357\",\"phoneNumber\": \"8560010014331\",\"email\": \"abc@gmail.com\",}", "9kw1wLMLG2u1j2zX");
+        logger.LogInformation($"encrypt: {encrypt}");
+
+        var decrypt = DotnetLib.Secure.SecureLogic.DecryptAes(encrypt, "9kw1wLMLG2u1j2zX");
+        logger.LogInformation($"decrypt: {decrypt}");
         // Start background refresh task
         Task.Run(() => Esim.Apis.Singleton.ConfigManager.Instance.RefreshConfigs());
     }
@@ -105,6 +125,7 @@ using (var scope = app.Services.CreateScope())
 }
 app.UseSwagger();
 app.UseSwaggerUI();
+
 // Configure the HTTP request pipeline.
 //if (app.Environment.IsDevelopment())
 //{
@@ -132,14 +153,11 @@ app.UseCors("AllowFrontend");
 app.UseAuthentication();
 app.UseAuthorization();
 
-app.MapStaticAssets();
+//app.MapStaticAssets();
 
-app.MapControllerRoute(
-    name: "default",
-    pattern: "{controller=Home}/{action=Index}/{id?}")
-    .WithStaticAssets();
+app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
+    //.WithStaticAssets();
 
 app.MapControllers();
 
 app.Run();
-

+ 2 - 7
EsimLao/Esim.Apis/Properties/PublishProfiles/FolderProfile.pubxml

@@ -2,19 +2,14 @@
 <!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
 <Project>
   <PropertyGroup>
-    <DeleteExistingFiles>true</DeleteExistingFiles>
+    <DeleteExistingFiles>false</DeleteExistingFiles>
     <ExcludeApp_Data>false</ExcludeApp_Data>
     <LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
     <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
     <LastUsedPlatform>Any CPU</LastUsedPlatform>
     <PublishProvider>FileSystem</PublishProvider>
-    <PublishUrl>D:\Code\Ex_publish\Lao\Esim\apis</PublishUrl>
+    <PublishUrl>bin\Release\net7.0\publish\</PublishUrl>
     <WebPublishMethod>FileSystem</WebPublishMethod>
     <_TargetId>Folder</_TargetId>
-    <SiteUrlToLaunchAfterPublish />
-    <TargetFramework>net9.0</TargetFramework>
-    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
-    <ProjectGuid>f7de4e65-3ca0-47dc-89fc-727cdf7558b8</ProjectGuid>
-    <SelfContained>true</SelfContained>
   </PropertyGroup>
 </Project>

+ 5 - 3
EsimLao/Esim.Apis/appsettings.json

@@ -1,5 +1,5 @@
 {
-  "Connection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1539))(CONNECT_DATA=(SERVICE_NAME=ORA12C)));User Id=laos_esim;Password=laos_esim;Connection Timeout=120;",
+  "Connection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1539))(CONNECT_DATA=(SERVICE_NAME=ORCL.viettech.local)));User Id=laos_esim;Password=laos_esim;Connection Timeout=120;",
   "Logging": {
     "LogLevel": {
       "Default": "Information",
@@ -30,6 +30,8 @@
     "ClientId": "510004579352-p8brlhu92qesea636ae2c1k96bq30u0j.apps.googleusercontent.com",
     "ClientSecret": "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",
     "RedirectUri": "https://simgetgo.vn"
+  },
+  "Partner": {
+    "EncryptionKey": "9kw1wLMLG2u1j2zX"
   }
-
-}
+}

Різницю між файлами не показано, бо вона завелика
+ 269 - 0
EsimLao/docs/.$skyhub.drawio.bkp


BIN
EsimLao/docs/language.xlsx


Різницю між файлами не показано, бо вона завелика
+ 269 - 0
EsimLao/docs/skyhub.drawio


BIN
EsimLao/docs/ssl/simgetgo_vn.zip


+ 34 - 0
EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_RootCA.crt

@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
+iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
+cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
+BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
+MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
+dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
+3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
+tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
+Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
+VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
+79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
+c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
+Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
+c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
+UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
+Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
+BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
+A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
+Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
+VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
+ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
+8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
+iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
+Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
+XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
+qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
+VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
+L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
+jjxDah2nGN59PRbxYvnKkKj9
+-----END CERTIFICATE-----

+ 36 - 0
EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_cert.crt

@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIGQTCCBSmgAwIBAgIRAIvdsEK54uwrvAo7oE5Y4QUwDQYJKoZIhvcNAQELBQAw
+gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE
+AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD
+QTAeFw0yNjAxMDcwMDAwMDBaFw0yNzAyMDcyMzU5NTlaMBYxFDASBgNVBAMTC3Np
+bWdldGdvLnZuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEUz7+Ln
+9DGHd33SHgDZT3Gq0STM/p3gBInHfk5EEGLc/iRsqoCJYIR4ihZwOlRErE1di+tY
+UCekzUfnsqPVSi2FbaTYXHsKOmqMQ+Dr+OW/rIZeY7b5/QJhec88u4vcYEUFScjE
+DjDOEYPe7OZLgcvfQsuITZdwUyTGjWiVX8A71wqXNblm8dftpqLnLOIRYx6D1H+m
+3rT/IvaeGl2b47MvvI9cQgLWVkLYkrcfherfZEdfugfU2NbZyaJ/sYLEuff3ryRz
+Lc7XN3ga/aZbRwfyMM/Q4KFE6lgQeZIK4cOKcid4DWBpswAFywe/VLTan4twk7b7
+V8n2SDzLq90q0QIDAQABo4IDDjCCAwowHwYDVR0jBBgwFoAUjYxexFStiuF36Zv5
+mwXhuAGNYeEwHQYDVR0OBBYEFHVNyTXZQzoMfn+Dad8oGpc73OsSMA4GA1UdDwEB
+/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+BQcDAjBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgIHMCUwIwYIKwYBBQUHAgEWF2h0
+dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBhAYIKwYBBQUHAQEEeDB2
+ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29SU0FE
+b21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0MCMGCCsGAQUFBzABhhdo
+dHRwOi8vb2NzcC5zZWN0aWdvLmNvbTAnBgNVHREEIDAeggtzaW1nZXRnby52boIP
+d3d3LnNpbWdldGdvLnZuMIIBjgYKKwYBBAHWeQIEAgSCAX4EggF6AXgAdwBgTJqv
+en93XwHUBvySDciZ6wscffjJUhv6+hd3O5eLyQAAAZuWG4M7AAAEAwBIMEYCIQD2
+Gm7YRVPTrA0OeRPzjlqfBrqomqi+gt55ex0ssmL8VAIhAMeVacY7Nj2qkPoKAz91
+GfyZaVVaCwrrVJe051xx/B6/AH0AjspHC6zeavOiBrCkeoS3Rv4fxr+VPiXmm07k
+AkjzxugAAAGblhuDegAIAAAFAAEkK3wEAwBGMEQCIGlO8q0wKV/6XWXqSfoKl/c9
+NnZGXRS65mLvs4ndvauoAiAK1/WSLuYl5MOh+xzHlQqaag4M/niVv5tmrEoQM5hl
+QgB+AFlubDOGlLJZcqJWyKDo3ZBKdugIPdqHOwEIOCgUPO5ZAAABm5YbhIIACAAA
+BQAACQ3qBAMARzBFAiBpdXIYgu+u7Ln7UCAgiwvIWkr9UF+0d6FdFsTu9bfURgIh
+ALBcLH/hrBg+He3mNN/mP0lst/Xj8bA8RQR4cmTYvu5LMA0GCSqGSIb3DQEBCwUA
+A4IBAQAgT24oAce5mYKk3U/x8e8QeZwfcuTu/id4O2DPM2jNCrMODYlvh8uKi9c/
+oI+Gjn0WPVywKmuinXRYViSM2cpaV4lWS7YSIC6kl/2YOscfPSIetXGf1QsZUSmz
+RBsR9yLZA91almf/4MN7GsbAC2LsXKAaUvnce1A+S21HWOw3BjAWkzJ+iRGzhLEa
+V1vcEs45tdFqqzep98S4MrcAAEDglcYNftds5N2WoZN69DEUZH+XnFyRJuFvQ4uj
+BwVKN0pbAqTfdgaQdg1LnCdLAYPeW39+pU/Np2kNfnvu+uk8RwlLgBmG+AjrYiCx
+LQ97Eeu1g7PateuPeV9n+aesiwST
+-----END CERTIFICATE-----

+ 105 - 0
EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_cert_inter_root.crt

@@ -0,0 +1,105 @@
+-----BEGIN CERTIFICATE-----
+MIIGQTCCBSmgAwIBAgIRAIvdsEK54uwrvAo7oE5Y4QUwDQYJKoZIhvcNAQELBQAw
+gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE
+AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD
+QTAeFw0yNjAxMDcwMDAwMDBaFw0yNzAyMDcyMzU5NTlaMBYxFDASBgNVBAMTC3Np
+bWdldGdvLnZuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEUz7+Ln
+9DGHd33SHgDZT3Gq0STM/p3gBInHfk5EEGLc/iRsqoCJYIR4ihZwOlRErE1di+tY
+UCekzUfnsqPVSi2FbaTYXHsKOmqMQ+Dr+OW/rIZeY7b5/QJhec88u4vcYEUFScjE
+DjDOEYPe7OZLgcvfQsuITZdwUyTGjWiVX8A71wqXNblm8dftpqLnLOIRYx6D1H+m
+3rT/IvaeGl2b47MvvI9cQgLWVkLYkrcfherfZEdfugfU2NbZyaJ/sYLEuff3ryRz
+Lc7XN3ga/aZbRwfyMM/Q4KFE6lgQeZIK4cOKcid4DWBpswAFywe/VLTan4twk7b7
+V8n2SDzLq90q0QIDAQABo4IDDjCCAwowHwYDVR0jBBgwFoAUjYxexFStiuF36Zv5
+mwXhuAGNYeEwHQYDVR0OBBYEFHVNyTXZQzoMfn+Dad8oGpc73OsSMA4GA1UdDwEB
+/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+BQcDAjBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgIHMCUwIwYIKwYBBQUHAgEWF2h0
+dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBhAYIKwYBBQUHAQEEeDB2
+ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29SU0FE
+b21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0MCMGCCsGAQUFBzABhhdo
+dHRwOi8vb2NzcC5zZWN0aWdvLmNvbTAnBgNVHREEIDAeggtzaW1nZXRnby52boIP
+d3d3LnNpbWdldGdvLnZuMIIBjgYKKwYBBAHWeQIEAgSCAX4EggF6AXgAdwBgTJqv
+en93XwHUBvySDciZ6wscffjJUhv6+hd3O5eLyQAAAZuWG4M7AAAEAwBIMEYCIQD2
+Gm7YRVPTrA0OeRPzjlqfBrqomqi+gt55ex0ssmL8VAIhAMeVacY7Nj2qkPoKAz91
+GfyZaVVaCwrrVJe051xx/B6/AH0AjspHC6zeavOiBrCkeoS3Rv4fxr+VPiXmm07k
+AkjzxugAAAGblhuDegAIAAAFAAEkK3wEAwBGMEQCIGlO8q0wKV/6XWXqSfoKl/c9
+NnZGXRS65mLvs4ndvauoAiAK1/WSLuYl5MOh+xzHlQqaag4M/niVv5tmrEoQM5hl
+QgB+AFlubDOGlLJZcqJWyKDo3ZBKdugIPdqHOwEIOCgUPO5ZAAABm5YbhIIACAAA
+BQAACQ3qBAMARzBFAiBpdXIYgu+u7Ln7UCAgiwvIWkr9UF+0d6FdFsTu9bfURgIh
+ALBcLH/hrBg+He3mNN/mP0lst/Xj8bA8RQR4cmTYvu5LMA0GCSqGSIb3DQEBCwUA
+A4IBAQAgT24oAce5mYKk3U/x8e8QeZwfcuTu/id4O2DPM2jNCrMODYlvh8uKi9c/
+oI+Gjn0WPVywKmuinXRYViSM2cpaV4lWS7YSIC6kl/2YOscfPSIetXGf1QsZUSmz
+RBsR9yLZA91almf/4MN7GsbAC2LsXKAaUvnce1A+S21HWOw3BjAWkzJ+iRGzhLEa
+V1vcEs45tdFqqzep98S4MrcAAEDglcYNftds5N2WoZN69DEUZH+XnFyRJuFvQ4uj
+BwVKN0pbAqTfdgaQdg1LnCdLAYPeW39+pU/Np2kNfnvu+uk8RwlLgBmG+AjrYiCx
+LQ97Eeu1g7PateuPeV9n+aesiwST
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB
+iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
+cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
+BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx
+MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV
+BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE
+ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g
+VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N
+TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj
+eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E
+oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk
+Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY
+uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j
+BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb
++ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G
+A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw
+CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0
+LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr
+BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv
+bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov
+L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H
+ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH
+7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi
+H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx
+RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv
+xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38
+sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL
+l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq
+6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY
+LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5
+yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K
+00u/I5sUKUErmgQfky3xxzlIPK1aEn8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
+iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
+cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
+BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
+MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
+dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
+3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
+tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
+Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
+VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
+79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
+c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
+Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
+c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
+UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
+Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
+BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
+A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
+Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
+VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
+ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
+8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
+iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
+Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
+XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
+qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
+VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
+L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
+jjxDah2nGN59PRbxYvnKkKj9
+-----END CERTIFICATE-----

+ 18 - 0
EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_csr.csr

@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIC7zCCAdcCAQAwgakxCzAJBgNVBAYTAlZOMQ8wDQYDVQQIDAZIYSBOb2kxDzAN
+BgNVBAcMBkhhIE5vaTEvMC0GA1UECgwmQ09ORyBUWSBUTkhIIEdJQUkgUEhBUCBW
+SUVOIFRIT05HIFZJRVQxCzAJBgNVBAsMAklUMRQwEgYDVQQDDAtzaW1nZXRnby52
+bjEkMCIGCSqGSIb3DQEJARYVc3VwcG9ydEB2aWV0dGVjaC5hc2lhMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEUz7+Ln9DGHd33SHgDZT3Gq0STM/p3g
+BInHfk5EEGLc/iRsqoCJYIR4ihZwOlRErE1di+tYUCekzUfnsqPVSi2FbaTYXHsK
+OmqMQ+Dr+OW/rIZeY7b5/QJhec88u4vcYEUFScjEDjDOEYPe7OZLgcvfQsuITZdw
+UyTGjWiVX8A71wqXNblm8dftpqLnLOIRYx6D1H+m3rT/IvaeGl2b47MvvI9cQgLW
+VkLYkrcfherfZEdfugfU2NbZyaJ/sYLEuff3ryRzLc7XN3ga/aZbRwfyMM/Q4KFE
+6lgQeZIK4cOKcid4DWBpswAFywe/VLTan4twk7b7V8n2SDzLq90q0QIDAQABoAAw
+DQYJKoZIhvcNAQELBQADggEBAJTDf9ostJcC+MefY4BHBkWBrQ8i1XEK8UHIzJiu
+caMVvgpLnSPeYXWf7fQ/HLuNjF8oMNrWpHGsMhnN0jdLcUHhUn/Jdu+7pwepuecv
+O+KzatTklsO2Rp819VWhE3o19BoQ0AVvssLogN2yKkqbrRM7deXegF9KVadlWmr8
+fgi0RDZhB+RavWa5E46U5bNAUIxEr5IbigZDZrVS3KQKDoMRN++y8FifnZKZoeY+
+1ChUr6+GnCPexGXFFKYYh4oY/V21r3P1e0vNjEIsdxIqPsBysKkC04pPAkn0F6/I
+LayQ8yDa2kGlM0tp0s1EwBvGNKIhs9orEj7R5z6ZZq60xiA=
+-----END CERTIFICATE REQUEST-----

+ 1 - 0
EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_passwordpfx.txt

@@ -0,0 +1 @@
+4XwYwOUOO5

+ 28 - 0
EsimLao/docs/ssl/simgetgo_vn/simgetgo_vn_private_key.key

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwRTPv4uf0MYd3
+fdIeANlPcarRJMz+neAEicd+TkQQYtz+JGyqgIlghHiKFnA6VESsTV2L61hQJ6TN
+R+eyo9VKLYVtpNhcewo6aoxD4Ov45b+shl5jtvn9AmF5zzy7i9xgRQVJyMQOMM4R
+g97s5kuBy99Cy4hNl3BTJMaNaJVfwDvXCpc1uWbx1+2moucs4hFjHoPUf6betP8i
+9p4aXZvjsy+8j1xCAtZWQtiStx+F6t9kR1+6B9TY1tnJon+xgsS59/evJHMtztc3
+eBr9pltHB/Iwz9DgoUTqWBB5kgrhw4pyJ3gNYGmzAAXLB79UtNqfi3CTtvtXyfZI
+PMur3SrRAgMBAAECggEAbXW759Z3BdwP7HL0nO8YA05prdB1gBOXvhm1CUke2TIb
+W7JQzJxfMtFwbQe8D2yu7HRqh+3QfI3xuAxT5En6tEKKXekE9oDtfBMYITq2Lmy1
+VzRlvvQsTyJAV70KUMhsRFf4DbafkNRb0uKZ93CXKPXqIXd0oAJPnUH0DnIIeFlG
+i1HlEHg7GlZc7U/SBeYZ5GYauVexpK1gOpZ9WDqpfaex1ZR1hYG9S42zgY2gM0nl
+fFAz9g4W18lU8uDA5GkmijQA7jptZRUTggqJ/Hq0GQF+XV2Ysj+3d/EpHSd4QCKc
+Cvg9qAHGfemlr0r/DdT8DQADtMGuGrJdAv/i1ED+QQKBgQDcmozVnEx6jyEOYO2p
+ZX+1Qc6F4OH9QbPIAM2lo1F8gnslrYeBs0Xx3Ih74HfWWLZk4nAMNQaoRwDU1zzV
+jLr8M3q60r/TzxQfPnDCpybYxuqsPJH4CmY5TorF3/xU4VCxrJMrC/ISbIxEK0bj
+PMnyQ4OC8R/3WDsOZycFP+0m6QKBgQDMjaDSoAdOCLb9cWvmSZ6ivi6m8+V0cdCX
+W5Y3WHUHd854jDUk10u+AGr25BkERBHocWmewEDiQXtBx6fij6djWtL+aJDb4vz3
+HwqNkSoJ4lDyTI8Uqmv5SW59FA4WbF+sK1ZPtXpnGpHNQV45Z/mj9tStDzW97WBF
+SgwR8XPDqQKBgQCoWhGsWtFKnrwdWv9ETT8+tm/DX7FXMrLeqS5Hf/GJ0ZHlWR6W
+bpesUh6eWtlZbO4U43Ga/0SLW1RZGcFFP1bbbPX2aSsXm2rBxUcMZb+Ro4wn0Tmc
+T2NfzH6/L4ZDyIdqcEcHjOA6CzioNVatyBBdoARXA2jfvCelgDcZHIYBMQKBgEjT
+n899hDxHJAdl61mJGn6soWV1blsf7nkE9Z1s0/W9tPf8Syt7r47NDUgre7CmH44X
+WthXcIJdd+m6kM48AQeBnZ9QUUrILPI6zgILDeGI/vLVSRe9eUW+3CSSEFXxbbqZ
+j1g/r32n2wMt48MifqHezmdzA9pD+kOREm0M/v9JAoGBALNVBbDcV3DJ4VuK9XOm
+0H09+9mUI92Qs3tUShJVjhSzRv9VdH2+cn7cgqXhfipnjto56Y7KAnPJ0iax9VwS
++ffnVlMDNp2J0VqUpfibQ8rU+RfEuqIG2J1wlzMu9eKipN+RRdMjL5TD2tEsDthL
+LxMq89fXimIEf31yxhh03ed0
+-----END PRIVATE KEY-----

+ 24 - 0
EsimLao/esim-vite/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 20 - 0
EsimLao/esim-vite/README.md

@@ -0,0 +1,20 @@
+<div align="center">
+<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
+</div>
+
+# Run and deploy your AI Studio app
+
+This contains everything you need to run your app locally.
+
+View your app in AI Studio: https://ai.studio/apps/drive/1Iycz8nBgG9XSpMkhPpCmBFxU6DXVxcvT
+
+## Run Locally
+
+**Prerequisites:**  Node.js
+
+
+1. Install dependencies:
+   `npm install`
+2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
+3. Run the app:
+   `npm run dev`

Різницю між файлами не показано, бо вона завелика
+ 33 - 0
EsimLao/esim-vite/SkySim/SkySim/Logo Skysim.ai


+ 78 - 0
EsimLao/esim-vite/SkySim/SkySim/Logo Skysim.svg

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3006.17 1200">
+  <!-- Generator: Adobe Illustrator 29.8.3, SVG Export Plug-In . SVG Version: 2.1.1 Build 3)  -->
+  <defs>
+    <style>
+      .st0 {
+        fill: url(#linear-gradient2);
+      }
+
+      .st1 {
+        fill: #ed2024;
+      }
+
+      .st2 {
+        fill: url(#linear-gradient1);
+      }
+
+      .st3 {
+        fill: #e61e25;
+      }
+
+      .st4 {
+        fill: url(#linear-gradient);
+      }
+    </style>
+    <linearGradient id="linear-gradient" x1="2456.77" y1="855.18" x2="2951.25" y2="1129.08" gradientTransform="translate(0 1199.28) scale(1 -1)" gradientUnits="userSpaceOnUse">
+      <stop offset="0" stop-color="#ec1d24"/>
+      <stop offset="1" stop-color="#ee3728"/>
+    </linearGradient>
+    <linearGradient id="linear-gradient1" x1="2036.44" y1="774.51" x2="2420" y2="894.85" gradientTransform="translate(0 1199.28) scale(1 -1)" gradientUnits="userSpaceOnUse">
+      <stop offset=".08" stop-color="#fff"/>
+      <stop offset=".32" stop-color="#f9beaa"/>
+      <stop offset=".56" stop-color="#f47c53"/>
+      <stop offset="1" stop-color="#af1f23"/>
+    </linearGradient>
+    <linearGradient id="linear-gradient2" x1="2265.3" y1="816.43" x2="2455.38" y2="816.43" gradientTransform="translate(0 1199.28) scale(1 -1)" gradientUnits="userSpaceOnUse">
+      <stop offset=".05" stop-color="#fff"/>
+      <stop offset=".56" stop-color="#f47c53"/>
+      <stop offset="1" stop-color="#af1f23"/>
+    </linearGradient>
+  </defs>
+  <g>
+    <path class="st3" d="M169.34,1091.13h34.48v-91.92h-28.47v-15.94c15.3-2.55,25.86-6.16,35.54-11.53h20.82v119.39h30.31v20.69h-92.69v-20.69h.01Z"/>
+    <path class="st3" d="M285.89,1047.11h57.53v18.49h-57.53v-18.49Z"/>
+    <path class="st3" d="M365.41,1040.33c0-47.1,32.44-75.1,72.54-75.1,19.85,0,35.64,8.63,45.62,18.22l-15.01,16.46c-8.14-7.4-17.63-12.5-30.02-12.5-25.76,0-44.26,19.81-44.26,52.12s17.05,52.74,43.58,52.74c14.04,0,24.89-5.9,33.8-14.79l15.01,16.02c-12.83,13.56-29.35,20.96-49.88,20.96-39.81,0-71.38-26.5-71.38-74.13h0Z"/>
+    <path class="st3" d="M510.77,1084v-127.66h27.89v128.98c0,6.25,3.1,8.45,6,8.45,1.26,0,2.23,0,4.36-.44l3.49,18.93c-3.39,1.23-8.04,2.2-14.72,2.2-19.76,0-27.02-11.62-27.02-30.46Z"/>
+    <path class="st3" d="M573.19,970.42c0-8.54,7.36-14.35,17.24-14.35s17.24,5.81,17.24,14.35-7.36,14.35-17.24,14.35-17.24-5.99-17.24-14.35ZM576.39,1003.7h27.89v108.12h-27.89v-108.12h0Z"/>
+    <path class="st3" d="M631.98,1057.85c0-36.01,27.99-56.79,59.66-56.79,15.11,0,26.15,5.28,34.77,12.15l-13.56,16.46c-6.3-5.02-12.3-7.92-19.76-7.92-19.18,0-32.44,14.44-32.44,36.1s13.07,35.92,31.57,35.92c9.3,0,17.82-4.14,24.6-9.33l11.33,16.73c-11.14,8.98-25.28,13.29-38.84,13.29-32.44,0-57.34-20.78-57.34-56.61h.01Z"/>
+    <path class="st3" d="M751.68,956.34h27.31v97.11h.77l44.07-49.74h30.51l-40.77,44.02,44.94,64.1h-30.31l-30.6-46.93-18.6,19.46v27.47h-27.31v-155.48h-.01Z"/>
+    <path class="st3" d="M960.87,989.35h-45.71v-21.48h119.51v21.48h-45.71v122.47h-28.09v-122.47h0Z"/>
+    <path class="st3" d="M1033.65,1057.85c0-36.01,26.92-56.79,56.56-56.79s56.56,20.78,56.56,56.79-26.92,56.61-56.56,56.61-56.56-20.78-56.56-56.61ZM1118.11,1057.85c0-21.66-10.46-36.1-27.89-36.1s-27.89,14.44-27.89,36.1,10.46,35.92,27.89,35.92,27.89-14.26,27.89-35.92Z"/>
+    <path class="st3" d="M1255.78,989.35h-45.71v-21.48h119.51v21.48h-45.71v122.47h-28.09v-122.47h0Z"/>
+    <path class="st3" d="M1353.59,956.34h27.89v40.24l-1.16,20.87c9.98-8.54,21.79-16.38,38.06-16.38,25.47,0,36.61,15.67,36.61,42.97v67.79h-27.89v-64.54c0-17.78-5.62-24.48-18.5-24.48-10.46,0-17.34,4.67-27.12,13.47v75.54h-27.89v-155.48h0Z"/>
+    <path class="st3" d="M1480.85,1057.85c0-35.13,26.83-56.79,54.82-56.79,32.16,0,49.1,21.13,49.1,51.33,0,4.84-.58,9.68-1.21,12.33h-75.5c2.32,19.19,15.88,30.11,35.06,30.11,10.17,0,18.89-2.91,27.6-7.84l9.49,15.76c-11.33,6.96-25.67,11.71-40.77,11.71-32.83,0-58.59-20.96-58.59-56.61ZM1560.66,1047.46c0-16.64-8.13-26.77-24.41-26.77-13.85,0-26.05,9.42-28.47,26.77h52.88Z"/>
+    <path class="st3" d="M1646.51,967.87h28.86l13.75,72.72c2.57,15.32,5.28,30.64,7.84,45.96h.97c3.29-15.32,6.78-30.77,10.27-45.96l19.47-72.72h24.41l19.47,72.72c3.49,14.92,6.97,30.51,10.46,45.96h.97c2.57-15.45,5.08-30.9,7.65-45.96l13.75-72.72h26.92l-30.41,143.95h-34.77l-19.66-75.8c-2.61-11.18-4.94-22.19-6.97-33.1h-.97c-2.18,10.92-4.55,21.92-7.17,33.1l-19.18,75.8h-34.19l-31.48-143.95h.01Z"/>
+    <path class="st3" d="M1845.73,1057.85c0-36.01,26.92-56.79,56.56-56.79s56.56,20.78,56.56,56.79-26.92,56.61-56.56,56.61-56.56-20.78-56.56-56.61ZM1930.18,1057.85c0-21.66-10.46-36.1-27.89-36.1s-27.89,14.44-27.89,36.1,10.46,35.92,27.89,35.92,27.89-14.26,27.89-35.92Z"/>
+    <path class="st3" d="M1986.5,1003.7h22.86l2.13,19.28h.78c8.47-14.09,21.06-21.92,33.8-21.92,6.05,0,9.88.71,13.46,2.2l-4.84,22.01c-4.16-1.06-7.26-1.76-12.2-1.76-9.49,0-20.92,5.9-28.09,22.36v65.94h-27.89v-108.12h-.01Z"/>
+    <path class="st3" d="M2076.81,1084v-127.66h27.89v128.98c0,6.25,3.1,8.45,6,8.45,1.26,0,2.23,0,4.36-.44l3.49,18.93c-3.39,1.23-8.04,2.2-14.72,2.2-19.76,0-27.02-11.62-27.02-30.46Z"/>
+    <path class="st3" d="M2135.16,1057.85c0-35.22,24.31-56.79,50.36-56.79,13.46,0,21.99,4.58,31.19,11.89l-1.07-17.43v-39.18h27.89v155.48h-22.86l-2.13-11.71h-.78c-8.81,8.01-21.02,14.35-33.8,14.35-29.64,0-48.81-20.96-48.81-56.61h.01ZM2215.64,1081.36v-50.36c-8.13-6.6-16.17-8.98-24.21-8.98-15.11,0-27.6,13.03-27.6,35.57s9.88,35.92,27.02,35.92c9.2,0,16.95-3.7,24.79-12.15Z"/>
+  </g>
+  <rect class="st1" x="1925.92" y="351.5" width="103.56" height="479.79" rx="25.77" ry="25.77"/>
+  <g>
+    <path class="st1" d="M883.19,487.3c-17.87,18.98-37.46,36.97-55.04,56.28-2.99,3.28-15.69,17.34-16.82,20.1-.77,1.87-.97,3.6-.37,5.55l63.61,74.62c41.2,48.68,87.22,95.56,127.12,145.09,4.17,5.18,18.86,21.22,12.44,27.68-1.64,1.66-12.12,3.24-15.01,3.45-23.76,1.75-66.78,3.38-89.21-1.61-15.19-3.38-29.16-22.49-39.57-34.29-41.16-46.68-79.17-96.69-121.31-142.47-12.92-14.04-12.82-5.15-22.67,4.79-5.73,5.79-33.49,30-34.62,35.01-2.58,38.1,2.66,79.2-.18,116.94-1.17,15.45-7.11,21.46-22.69,22.68-12.93,1.01-53.9,1.59-64.97-1.58-9.25-2.65-11.95-8.93-12.66-17.94v-413.69c1.84-11.74,2.05-23.85,14.25-28.99,10.82-4.56,45.45-5.12,58.1-4.24,17.07,1.19,26.41,8.1,27.95,25.86,3.33,38.28-.62,85.57-1.14,124.5-.16,12.42-1.02,28.13,0,40.18.25,2.95.16,6.11,3.74,6.42,1.49-.14,4.97-2.76,6.31-3.85,14.44-11.77,29.69-32.8,42.71-46.95,31.56-34.27,63.3-68.19,95.55-101.78,15.37-16.01,27.83-36.42,50.71-42.14,16.45-4.12,68.72-5.97,83.48.78,6.54,2.99,11.03,9.89,9.33,17.08-2.46,10.41-31.32,40.02-40.11,49.61-19.35,21.12-39.29,42.05-58.94,62.92h0Z"/>
+    <path class="st1" d="M1590.54,454.2c-12.44,11.47-16.47,29.97-9.21,45.05,10.41,21.64,33.16,28.18,55.04,33.99,86.24,22.89,221.94,23.86,233.39,137.45,18.67,185.16-245.88,185.88-361.91,123.18-14.82-8.01-44.8-26.85-44.21-45.46.29-9.17,15.22-39.05,22.57-45.15,17.17-14.25,52.91,11.11,69.48,18.84,54.65,25.49,128.38,42.04,185.73,15.19,47.07-22.03,40.21-73.06-6.73-91.43-75.56-29.57-208.29-18.22-245.83-105.58-14.78-34.4-13.49-79.26,5.11-111.98,54.41-95.73,219.11-88.75,307.94-50.21,15.51,6.73,43.06,19.23,44.66,37.97,1.17,13.74-13.53,46.25-29.28,48.91-15.25,2.58-45.01-12.5-60.66-17.63-46.05-15.1-126.97-29.24-166.09,6.86h0Z"/>
+    <path class="st1" d="M485.73,460.75c-15.8,13.01-54.09-8.26-71.17-13.88-46.81-15.4-118.12-27.72-160.53,3.43-18.1,13.3-23.23,40-8.51,57.85,21.55,26.12,106.4,35.58,140.52,43.28,67.29,15.18,139.32,42.97,143.32,121.27,4.06,79.53-33.7,129.61-112.49,148.5-83.12,19.92-188.66,10.19-262.04-34.68-10.73-6.56-29.22-19.84-30.68-33.03s11.96-43.14,23.28-50.65c18.45-12.25,46.56,9.68,63.25,17.51,28.57,13.41,62.96,25.03,94.55,28.35,45.68,4.79,133.49-.88,126.99-64.55-3.23-31.64-51.31-43.68-77.01-49.7-84.46-19.77-209-23.57-216.35-134.51-8.71-131.53,138.64-161.18,243.08-144.27,27.57,4.46,103.89,24.55,119.35,47.49,8.89,13.2.8,35.43-7.4,47.51-.76,1.12-7.61,9.61-8.18,10.08h.02Z"/>
+    <path class="st1" d="M1353.8,369.86c8.81-9.13,18.6-12.94,31.02-14.88,14.82-2.31,48.81-2.42,62.74,2.15,17.38,5.7,13.61,24.76,4.66,36.76-46.25,69.81-95.58,137.74-141.45,207.79-13.29,20.29-23.3,29.69-22.8,55.37.44,22.2.15,44.24,0,66.38-.17,24.23,2.24,53.66.09,77.11-1.25,13.62-8.74,18.95-21.69,20.51-13.93,1.68-27.32.73-41.09-.03-18.26-1-34.8,2.48-37.49-21.6-3.45-50.63,4.56-107.33.03-157.24-1.27-14.01-14.89-28.48-22.43-39.82-47.54-71.52-96.27-142.25-144.13-213.55-5.82-8.5-10.08-23.16.61-29.62,12.36-7.47,55.51-6.73,70.14-4.2,29.88,5.15,36.87,23.11,52.68,45.44,31.37,44.31,61.37,89.62,90.1,135.68,5.09,6.82,12.43-7.45,15.48-11.83,13.63-19.58,25.92-40.16,39.19-59.99,18.8-28.09,39.09-62.18,59.29-88.42,1.29-1.68,3.65-4.57,5.05-6.03v.02Z"/>
+  </g>
+  <path class="st1" d="M2233.11,807.24c-.51,3.96-5.62,16.18-8.52,18.79-3.69,3.31-14.51,4.7-19.45,5.07-14.34,1.05-37.18.1-51.73-1.05-14.95-1.19-20.54-6.78-21.63-22.75v-323.08c.52-6.3,1.66-11.47,7.5-14.31,32.02-11.58,65.44-20.42,96.32-35.2,7.3-3.49,16.45-12.5,24.11-8.68l110.55,137.06c4.47,5.54,8.59,12.78,13.19,18.3,3.24,3.88,13.36,16.26,17.89,15.57l2.73-1.68c13.81-17.74,26.46-36.52,40.09-54.39,38.2-50.07,75.95-100.49,114.95-149.9,7.41-9.39,18.22-28.06,28.62-33.16,11.91-5.83,42.05-6.6,55.29-5.11,9.45,1.06,18.51,10,18.51,19.64,0,51.17.28,91.65,0,137.37l-.06,300.84c-1.36,12.04-5.54,18.17-17.41,19.42-15.24,1.61-49.01,1.58-64.22-.13-9.61-1.08-14.74-5.4-17.06-15.35l-1.35-260.53c-.33-3.73-3.08-5.43-5.32-1.88-11.91,18.63-26.79,34.8-40.2,52.05-24.48,31.5-49.27,69.04-75.45,98.22-22.46,25.03-49.92,36.99-77.03,9.97-28.99-28.89-58.73-73.99-84.96-107.08-11.9-15.02-26-34.65-38.48-48.32-1.89-2.07-3.7-4.98-6.87-4.43v254.73h-.01Z"/>
+  <path class="st4" d="M2804.61,213.96c1.36,1.46,4.41,6.77,4.54,8.66,1.34,20.13-40.05,21.62-44,38.19-.74,3.1-2.86,17.22-1.85,19.26,2.18,4.41,23.9-.34,25.79,11.14,1.5,9.11-2.08,11.69-9.11,16.08-7.05,4.41-24.01,9.78-27.62,16.69-1.26,2.4-1.58,5.75-2.09,8.46-6.92,36.42-1.07,77.47-39.54,98.68-10.13,5.59-23.42,6.75-23.95-8.14-.41-11.37,3.31-26.25,4.25-37.93,3.44-42.73,6.42-87.02,2.1-129.76-.42-4.16-1.67-18.35-5.26-20.06-5.86-2.78-16.23,2.22-21.82,4.56-41.76,17.48-83.09,43.73-125.19,61.57-13.19,10.13-9.51,33.98-14.56,48.75-6.62,8.95-35.1,29.85-41.09,11.8-4.21-12.68-5.47-31.1-8.89-44.92-2.23-9.02-2.22-13.45-9.53-20.01-14.43-12.94-31.64-23.18-45.88-36.42-9.53-8.86-15.62-14.7.35-21.83,12.87-5.75,27.04-7.23,40.85-4.59,23.5,4.49,30.17,14.76,56.69,7.17,22.44-6.42,80.4-34.87,98.29-49.43,15.89-12.93,13.82-22.87-4.91-30.79-43.09-15.08-94.58-21.97-136.62-38.53-14.74-5.81-8.66-10.61,1.99-17.47,36.9-23.8,76.45-4.03,115.9-2.29,3.48.15,8.15.38,11.5-.08,10.81-1.48,24.47-19.33,33.78-25.31,7.11-2.51,12.57-1.42,15.58,5.68,3.24,7.63-1.5,23.98,2.18,27.9,6.97,7.41,32.05-4.7,39.86-8.66,10.88-5.51,36.92-26.76,45.91-8.02,3.07,6.4-1.35,23.73,6.18,23.29,5.74-.34,34.78-24.67,41.27-29.54,34.3-25.71,58.15-46.52,102.62-53.53,26.67-4.21,49.47-3.61,32.05,29.94-22.73,43.79-89.56,73.94-128.8,102.26-9.03,6.52-15.76,11.03-16.97,23.13-.29,2.85-.49,13.24.51,15.29,1.67,3.44,8.11,1.66,11.5,2.22,4.65.76,10.75,3.1,13.99,6.58h0Z"/>
+  <path class="st2" d="M2091.92,361.74l-12.98.84c-9.35.6-16.95,7.73-18.17,17.02-2.21,16.84-4.8,42.72-2.09,56.14.7,3.44,2.45,6.59,5.03,8.96,6.14,5.66,20.93,14.87,46.79,6.25,35.54-11.85,266-89.85,296.2-117.29,0,0-202.58,60.21-240.45,60.41,0,0-13.01-5.61-8.36-15.13,0,0,7.2-3.02,23-5.58s214.2-48.55,240.45-68.53c0,0-268.09,60.17-329.43,56.92h.01Z"/>
+  <path class="st0" d="M2266.19,430.79l.25.5c1.62,3.25,5.46,4.72,8.84,3.37,25.13-10.01,123.49-51.19,180.1-104.12,0,0-140.68,71.87-184.66,88.62-4.66,1.77-6.76,7.17-4.53,11.63h0Z"/>
+  <g>
+    <path class="st1" d="M1963.84,316.07c-5.6,0-10.16-5.01-10.16-11.16v-26.93c0-6.15,4.56-11.16,10.16-11.16s10.16,5.01,10.16,11.16v26.93c0,6.15-4.56,11.16-10.16,11.16Z"/>
+    <path class="st1" d="M2019.33,316.07c-5.6,0-10.16-5.01-10.16-11.16v-65.46c0-6.15,4.56-11.16,10.16-11.16s10.16,5.01,10.16,11.16v65.46c0,6.15-4.56,11.16-10.16,11.16Z"/>
+    <path class="st1" d="M2001.42,306.11c-.07,6-5.23,9.25-10.06,9.25-3.22,0-6.03-1.4-7.71-3.87-1.04-1.36-1.71-3.06-2.04-5.27-.07-.44-.07-.88-.07-1.4v-46.12c0-1.51.28-2.97.79-4.31,1.09-2.85,3.36-4.83,5.96-5.83,2.51-.97,5.52-.94,7.92.38,1.16.64,2.21,1.52,3.06,2.6,1.53,1.92,2.41,4.45,2.24,7.02.17,17.46.13,33-.1,47.55h0Z"/>
+    <path class="st1" d="M1936.09,285.76h0c5.61,0,10.16,4.55,10.16,10.16v9.99c0,5.61-4.55,10.16-10.16,10.16h0c-5.61,0-10.16-4.55-10.16-10.16v-9.99c0-5.61,4.55-10.16,10.16-10.16Z"/>
+  </g>
+</svg>

BIN
EsimLao/esim-vite/SkySim/SkySim/i.png


BIN
EsimLao/esim-vite/SkySim/SkySim/k.png


BIN
EsimLao/esim-vite/SkySim/SkySim/logo-skysim.png


BIN
EsimLao/esim-vite/SkySim/SkySim/m.png


BIN
EsimLao/esim-vite/SkySim/SkySim/s.png


BIN
EsimLao/esim-vite/SkySim/SkySim/text.png


BIN
EsimLao/esim-vite/SkySim/SkySim/y.png


BIN
EsimLao/esim-vite/dist.zip


BIN
EsimLao/esim-vite/favicon-48.png


BIN
EsimLao/esim-vite/favicon-96.png


BIN
EsimLao/esim-vite/favicon.ico


+ 80 - 0
EsimLao/esim-vite/index.html

@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <link rel="icon" type="image/svg+xml" href="./publish/assets/img/logo.png" />
+  <link rel="icon" href="/favicon.ico" sizes="any">
+  <link rel="icon" type="image/png" href="/favicon-48.png" sizes="48x48">
+  <link rel="icon" type="image/png" href="/favicon-96.png" sizes="96x96">
+  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>SkySimHub – Global eSIM & International SIM | Stay Connected Everywhere</title>
+  <meta name="description"
+    content="SkySimHub eSIM provides global eSIM and international SIM solutions. Receive QR via email, activate in 2 minutes, and stay connected worldwide without physical SIM." />
+  <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=Montserrat:ital,wght@0,100..900;1,100..900&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
+    rel="stylesheet">
+  <style>
+    body {
+      font-family: "Montserrat", 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="application/ld+json">
+  {
+    "@context": "https://schema.org",
+    "@type": "Organization",
+    "name": "SkySimHub",
+    "url": "https://skysimhub.vn",
+    "logo": "https://skysimhub.vn/assets/img/logo.png"
+  }
+  </script>
+  <script type="importmap">
+      {
+        "imports": {
+          "react/": "https://esm.sh/react@^19.2.3/",
+          "react": "https://esm.sh/react@^19.2.3",
+          "@google/genai": "https://esm.sh/@google/genai@^1.34.0",
+          "react-dom/": "https://esm.sh/react-dom@^19.2.3/",
+          "react-router-dom": "https://esm.sh/react-router-dom@^7.11.0"
+        }
+      }
+    </script>
+  <link rel="stylesheet" href="./src/index.css" />
+</head>
+
+<body>
+  <div id="root"></div>
+  <script type="module" src="./src/index.tsx"></script>
+</body>
+
+</html>

+ 7 - 0
EsimLao/esim-vite/metadata.json

@@ -0,0 +1,7 @@
+{
+  "name": "Esim",
+  "description": "Premium travel eSIM and SIM card solutions for global connectivity. High-speed 5G/4G coverage in over 200 countries.",
+  "requestFramePermissions": [
+    "geolocation"
+  ]
+}

+ 5195 - 0
EsimLao/esim-vite/package-lock.json

@@ -0,0 +1,5195 @@
+{
+  "name": "esim-vite",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "esim-vite",
+      "version": "0.0.0",
+      "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",
+        "crypto-js": "^4.2.0",
+        "i18next": "^25.7.4",
+        "meta": "^2.2.25",
+        "react": "^19.2.3",
+        "react-dom": "^19.2.3",
+        "react-i18next": "^16.5.2",
+        "react-redux": "^9.2.0",
+        "react-router-dom": "^7.11.0",
+        "redux": "^5.0.1",
+        "tailwindcss": "^4.1.18"
+      },
+      "devDependencies": {
+        "@types/node": "^22.14.0",
+        "@vitejs/plugin-react": "^5.0.0",
+        "typescript": "~5.8.2",
+        "vite": "^6.2.0"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+      "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.27.1",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+      "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+      "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.27.1",
+        "@babel/generator": "^7.28.5",
+        "@babel/helper-compilation-targets": "^7.27.2",
+        "@babel/helper-module-transforms": "^7.28.3",
+        "@babel/helpers": "^7.28.4",
+        "@babel/parser": "^7.28.5",
+        "@babel/template": "^7.27.2",
+        "@babel/traverse": "^7.28.5",
+        "@babel/types": "^7.28.5",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+      "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@babel/types": "^7.28.5",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.27.2",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+      "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.27.2",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+      "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.27.1",
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+      "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.27.1",
+        "@babel/traverse": "^7.28.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+      "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+      "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.27.2",
+        "@babel/types": "^7.28.4"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+      "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.5"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+      "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+      "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.27.2",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+      "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.27.1",
+        "@babel/parser": "^7.27.2",
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+      "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.27.1",
+        "@babel/generator": "^7.28.5",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.28.5",
+        "@babel/template": "^7.27.2",
+        "@babel/types": "^7.28.5",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+      "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@google/genai": {
+      "version": "1.34.0",
+      "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.34.0.tgz",
+      "integrity": "sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "google-auth-library": "^10.3.0",
+        "ws": "^8.18.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "@modelcontextprotocol/sdk": "^1.24.0"
+      },
+      "peerDependenciesMeta": {
+        "@modelcontextprotocol/sdk": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@reduxjs/toolkit": {
+      "version": "2.11.2",
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+      "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "@standard-schema/utils": "^0.3.0",
+        "immer": "^11.0.0",
+        "redux": "^5.0.1",
+        "redux-thunk": "^3.1.0",
+        "reselect": "^5.1.0"
+      },
+      "peerDependencies": {
+        "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+        "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "react": {
+          "optional": true
+        },
+        "react-redux": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.53",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+      "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
+      "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
+      "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
+      "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
+      "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
+      "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
+      "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
+      "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
+      "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
+      "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
+      "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
+      "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
+      "cpu": [
+        "loong64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
+      "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
+      "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
+      "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
+      "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
+      "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
+      "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
+      "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
+      "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
+      "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
+      "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
+      "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "license": "MIT"
+    },
+    "node_modules/@standard-schema/utils": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+      "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+      "license": "MIT"
+    },
+    "node_modules/@tailwindcss/node": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
+      "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.4",
+        "enhanced-resolve": "^5.18.3",
+        "jiti": "^2.6.1",
+        "lightningcss": "1.30.2",
+        "magic-string": "^0.30.21",
+        "source-map-js": "^1.2.1",
+        "tailwindcss": "4.1.18"
+      }
+    },
+    "node_modules/@tailwindcss/oxide": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
+      "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      },
+      "optionalDependencies": {
+        "@tailwindcss/oxide-android-arm64": "4.1.18",
+        "@tailwindcss/oxide-darwin-arm64": "4.1.18",
+        "@tailwindcss/oxide-darwin-x64": "4.1.18",
+        "@tailwindcss/oxide-freebsd-x64": "4.1.18",
+        "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
+        "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
+        "@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
+        "@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
+        "@tailwindcss/oxide-linux-x64-musl": "4.1.18",
+        "@tailwindcss/oxide-wasm32-wasi": "4.1.18",
+        "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
+        "@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-android-arm64": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
+      "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-arm64": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
+      "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-x64": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
+      "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-freebsd-x64": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
+      "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
+      "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
+      "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
+      "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
+      "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
+      "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
+      "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
+      "bundleDependencies": [
+        "@napi-rs/wasm-runtime",
+        "@emnapi/core",
+        "@emnapi/runtime",
+        "@tybys/wasm-util",
+        "@emnapi/wasi-threads",
+        "tslib"
+      ],
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1",
+        "@emnapi/wasi-threads": "^1.1.0",
+        "@napi-rs/wasm-runtime": "^1.1.0",
+        "@tybys/wasm-util": "^0.10.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+      "version": "1.7.1",
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.1.0",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+      "version": "1.7.1",
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+      "version": "1.1.0",
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.0",
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1",
+        "@tybys/wasm-util": "^0.10.1"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+      "version": "2.8.1",
+      "inBundle": true,
+      "license": "0BSD",
+      "optional": true
+    },
+    "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
+      "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
+      "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@tailwindcss/vite": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
+      "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tailwindcss/node": "4.1.18",
+        "@tailwindcss/oxide": "4.1.18",
+        "tailwindcss": "4.1.18"
+      },
+      "peerDependencies": {
+        "vite": "^5.2.0 || ^6 || ^7"
+      }
+    },
+    "node_modules/@tanstack/query-core": {
+      "version": "5.90.16",
+      "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz",
+      "integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
+    "node_modules/@tanstack/react-query": {
+      "version": "5.90.16",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz",
+      "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tanstack/query-core": "5.90.16"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": "^18 || ^19"
+      }
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "22.19.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
+      "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/use-sync-external-store": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+      "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+      "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.5",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.53",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.18.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+      "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/array-unique": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/async": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+      "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+      "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.9.11",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
+      "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.js"
+      }
+    },
+    "node_modules/bignumber.js": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
+        "node-releases": "^2.0.27",
+        "update-browserslist-db": "^1.2.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001762",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
+      "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+      "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/chalk/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "license": "MIT"
+    },
+    "node_modules/cli-cursor": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+      "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==",
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/cli-width": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
+      "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
+      "license": "ISC"
+    },
+    "node_modules/cliui": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+      "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^3.1.0",
+        "strip-ansi": "^5.2.0",
+        "wrap-ansi": "^5.1.0"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-regex": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+      "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/cliui/node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/cliui/node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+      "license": "MIT"
+    },
+    "node_modules/cliui/node_modules/emoji-regex": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+      "license": "MIT"
+    },
+    "node_modules/cliui/node_modules/is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+      "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^7.0.1",
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/cliui/node_modules/strip-ansi": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+      "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^3.2.0",
+        "string-width": "^3.0.0",
+        "strip-ansi": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "2.15.1",
+      "resolved": "git+ssh://git@github.com/mateodelnorte/commander.js.git#9060bf880b791cf39245d425f1e8a41a55616781",
+      "license": "MIT",
+      "dependencies": {
+        "find-module-bin": "^1.0.0"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/dedent": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
+      "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-file": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+      "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.267",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+      "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.18.4",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
+      "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es6-promisify": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz",
+      "integrity": "sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==",
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/expand-tilde": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+      "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+      "license": "MIT",
+      "dependencies": {
+        "homedir-polyfill": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/external-editor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+      "license": "MIT",
+      "dependencies": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fetch-blob": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "node-domexception": "^1.0.0",
+        "web-streams-polyfill": "^3.0.3"
+      },
+      "engines": {
+        "node": "^12.20 || >= 14.13"
+      }
+    },
+    "node_modules/figures": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+      "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==",
+      "license": "MIT",
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-module-bin": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/find-module-bin/-/find-module-bin-1.1.0.tgz",
+      "integrity": "sha512-v53mIFOiuump1BSkwURrLblQm+LpWDVQOkQdph/xLPRrbyCU48RryPUSYxih6+gkAdNw9AkqWL7OXUhmE1ddmg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "global-paths": "^1.0.0"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+      "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/findup-sync": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
+      "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
+      "license": "MIT",
+      "dependencies": {
+        "detect-file": "^1.0.0",
+        "is-glob": "^4.0.0",
+        "micromatch": "^4.0.2",
+        "resolve-dir": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/formdata-polyfill": {
+      "version": "4.0.10",
+      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+      "license": "MIT",
+      "dependencies": {
+        "fetch-blob": "^3.1.2"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "license": "ISC"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gaxios": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
+      "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "node-fetch": "^3.3.2",
+        "rimraf": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
+      "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "google-logging-utils": "^1.0.0",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-meta-file": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/get-meta-file/-/get-meta-file-1.3.6.tgz",
+      "integrity": "sha512-JExB2n7WZMj+tKaxgjAP7VkSN2Xbooa0ToyNa9/H7QFVa0vLbzHr0e1m5NX1IT/c01sDHO6JCGBsrON6stOozg==",
+      "license": "ISC",
+      "dependencies": {
+        "chalk": "^3.0.0",
+        "debug": "^4.3.2",
+        "dedent": "^0.7.0",
+        "findup-sync": "^4.0.0",
+        "prompt-sync": "^4.2.0",
+        "tildify": "^2.0.0"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/git-remote-origin-url": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-3.1.0.tgz",
+      "integrity": "sha512-yVSfaTMO7Bqk6Xx3696ufNfjdrajX7Ig9GuAeO2V3Ji7stkDoBNFldnWIAsy0qviUd0Z+X2P6ziJENKztW7cBQ==",
+      "license": "MIT",
+      "dependencies": {
+        "gitconfiglocal": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/gitconfiglocal": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz",
+      "integrity": "sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==",
+      "license": "BSD",
+      "dependencies": {
+        "ini": "^1.3.2"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+      "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/global-modules": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
+      "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==",
+      "license": "MIT",
+      "dependencies": {
+        "global-prefix": "^0.1.4",
+        "is-windows": "^0.2.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/global-modules/node_modules/is-windows": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz",
+      "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/global-paths": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/global-paths/-/global-paths-1.0.0.tgz",
+      "integrity": "sha512-L3Ctm3aer+82rIdINZrFkyF0+YJQeK+kHR5ilnOpnEzj6N/lnqxuYpl8GSP+oepPgOmHbOtEPy9w0ghRm6YSuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "array-unique": "^0.3.2",
+        "global-modules": "^0.2.3",
+        "is-windows": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/global-prefix": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz",
+      "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==",
+      "license": "MIT",
+      "dependencies": {
+        "homedir-polyfill": "^1.0.0",
+        "ini": "^1.3.4",
+        "is-windows": "^0.2.0",
+        "which": "^1.2.12"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/global-prefix/node_modules/is-windows": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz",
+      "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/global-prefix/node_modules/which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "which": "bin/which"
+      }
+    },
+    "node_modules/google-auth-library": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
+      "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "base64-js": "^1.3.0",
+        "ecdsa-sig-formatter": "^1.0.11",
+        "gaxios": "^7.0.0",
+        "gcp-metadata": "^8.0.0",
+        "google-logging-utils": "^1.0.0",
+        "gtoken": "^8.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/google-logging-utils": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
+      "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "license": "ISC"
+    },
+    "node_modules/gtoken": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
+      "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
+      "license": "MIT",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/homedir-polyfill": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+      "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+      "license": "MIT",
+      "dependencies": {
+        "parse-passwd": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/html-parse-stringify": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+      "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+      "license": "MIT",
+      "dependencies": {
+        "void-elements": "3.1.0"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/i18next": {
+      "version": "25.7.4",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz",
+      "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://locize.com"
+        },
+        {
+          "type": "individual",
+          "url": "https://locize.com/i18next.html"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4"
+      },
+      "peerDependencies": {
+        "typescript": "^5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/immer": {
+      "version": "11.1.3",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
+      "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "license": "ISC"
+    },
+    "node_modules/inquirer": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
+      "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^3.2.0",
+        "chalk": "^2.4.2",
+        "cli-cursor": "^2.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^2.0.0",
+        "lodash": "^4.17.12",
+        "mute-stream": "0.0.7",
+        "run-async": "^2.2.0",
+        "rxjs": "^6.4.0",
+        "string-width": "^2.1.0",
+        "strip-ansi": "^5.1.0",
+        "through": "^2.3.6"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/inquirer/node_modules/ansi-regex": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+      "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/inquirer/node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/inquirer/node_modules/chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/inquirer/node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/inquirer/node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+      "license": "MIT"
+    },
+    "node_modules/inquirer/node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/inquirer/node_modules/is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/inquirer/node_modules/string-width": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/inquirer/node_modules/string-width/node_modules/ansi-regex": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
+      "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/inquirer/node_modules/string-width/node_modules/strip-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+      "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/inquirer/node_modules/strip-ansi": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/inquirer/node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+      "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+      "license": "MIT",
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
+      "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.30.2",
+        "lightningcss-darwin-arm64": "1.30.2",
+        "lightningcss-darwin-x64": "1.30.2",
+        "lightningcss-freebsd-x64": "1.30.2",
+        "lightningcss-linux-arm-gnueabihf": "1.30.2",
+        "lightningcss-linux-arm64-gnu": "1.30.2",
+        "lightningcss-linux-arm64-musl": "1.30.2",
+        "lightningcss-linux-x64-gnu": "1.30.2",
+        "lightningcss-linux-x64-musl": "1.30.2",
+        "lightningcss-win32-arm64-msvc": "1.30.2",
+        "lightningcss-win32-x64-msvc": "1.30.2"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+      "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+      "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+      "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+      "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+      "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+      "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+      "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+      "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+      "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+      "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.30.2",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+      "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+      "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^3.0.0",
+        "path-exists": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/loop": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/loop/-/loop-3.3.6.tgz",
+      "integrity": "sha512-loP1rvosCzG3qHznhwH73LOBPcSlSx6bzRnu8ODeTyNIqWOcUUdtzDcM0LraCyT6WnZY/+Komf8NFBRKO6izsA==",
+      "license": "ISC",
+      "dependencies": {
+        "async": "^3.2.0",
+        "chalk": "^3.0.0",
+        "debug": "^4.3.2",
+        "lodash": "^4.17.21",
+        "meta-exec": "^1.4.2",
+        "should": "^13.2.3",
+        "yargs": "^14.2.3"
+      },
+      "bin": {
+        "loop": "bin/loop"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/meta": {
+      "version": "2.2.25",
+      "resolved": "https://registry.npmjs.org/meta/-/meta-2.2.25.tgz",
+      "integrity": "sha512-iKO5fCXYVzbMBAispFycgDDn/6/0F7ER2L8iPFIHbhgN/WtnM2zpdDHBUH0vnsPh0HO+GxmdZ75thIhIPSjHqg==",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "3.0.0",
+        "commander": "github:mateodelnorte/commander.js",
+        "debug": "4.3.2",
+        "meta-git": "1.1.7",
+        "meta-init": "1.2.5",
+        "meta-loop": "1.2.5",
+        "meta-project": "2.5.0",
+        "tabtab": "3.0.2",
+        "tildify": "2.0.0"
+      },
+      "bin": {
+        "meta": "bin/meta"
+      }
+    },
+    "node_modules/meta-exec": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/meta-exec/-/meta-exec-1.4.4.tgz",
+      "integrity": "sha512-rWGvtS4E22eCPlYNqEmW9buaTlxoOvbqiVbqLLxGvaamKRBRkgOalV1qmV+2w9O/giFPLqiEdffGmaGRqQNuFQ==",
+      "license": "ISC",
+      "dependencies": {
+        "chalk": "^3.0.0",
+        "debug": "^4.3.2",
+        "get-meta-file": "^1.3.4"
+      }
+    },
+    "node_modules/meta-git": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/meta-git/-/meta-git-1.1.7.tgz",
+      "integrity": "sha512-4v7v1MwfmjJOR6Ev9XzOGJEnV7wmltyQ+FjukE5bCDDBD7JQgNxtRmOJ+B7VhE55nMA1cGhu6J5l3CM0cEVuLw==",
+      "license": "ISC",
+      "dependencies": {
+        "chalk": "^3.0.0",
+        "commander": "^2.20.3",
+        "debug": "^4.3.2",
+        "get-meta-file": "^1.3.5",
+        "loop": "^3.3.4",
+        "meta-exec": "^1.4.2",
+        "meta-loop": "^1.2.2"
+      },
+      "bin": {
+        "meta-git": "bin/meta-git",
+        "meta-git-add": "bin/meta-git-add",
+        "meta-git-branch": "bin/meta-git-branch",
+        "meta-git-checkout": "bin/meta-git-checkout",
+        "meta-git-clean": "bin/meta-git-clean",
+        "meta-git-clone": "bin/meta-git-clone",
+        "meta-git-commit": "bin/meta-git-commit",
+        "meta-git-diff": "bin/meta-git-diff",
+        "meta-git-fetch": "bin/meta-git-fetch",
+        "meta-git-merge": "bin/meta-git-merge",
+        "meta-git-pull": "bin/meta-git-pull",
+        "meta-git-push": "bin/meta-git-push",
+        "meta-git-remote": "bin/meta-git-remote",
+        "meta-git-status": "bin/meta-git-status",
+        "meta-git-tag": "bin/meta-git-tag",
+        "meta-git-update": "bin/meta-git-update"
+      }
+    },
+    "node_modules/meta-git/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "license": "MIT"
+    },
+    "node_modules/meta-init": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/meta-init/-/meta-init-1.2.5.tgz",
+      "integrity": "sha512-0sD3GBldanS9s6EzGdKnDGP1y9eumDNQwEtY4ZZ09C63Y+g/fONCzAvuBHssvDzOpyHFnJWPhWK8VXlAQt+52w==",
+      "license": "ISC",
+      "dependencies": {
+        "async": "^3.2.0",
+        "debug": "^4.3.2",
+        "get-meta-file": "^1.3.4",
+        "git-remote-origin-url": "^3.1.0",
+        "inquirer": "^6.5.2"
+      },
+      "bin": {
+        "meta-init": "bin/meta-init"
+      }
+    },
+    "node_modules/meta-loop": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/meta-loop/-/meta-loop-1.2.5.tgz",
+      "integrity": "sha512-3GjhCmyLSOhyPuyAR3cEqFjcfDBRiCP2A4tstM7azC8pizaN0qb5QWQNCO0xtTguBqRCxXDkmUEfjRNc2WF93g==",
+      "license": "ISC",
+      "dependencies": {
+        "debug": "^4.3.2",
+        "get-meta-file": "^1.3.5",
+        "loop": "^3.3.5"
+      },
+      "bin": {
+        "meta-exec": "bin/meta-exec",
+        "meta-loop": "bin/meta-loop"
+      }
+    },
+    "node_modules/meta-project": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/meta-project/-/meta-project-2.5.0.tgz",
+      "integrity": "sha512-Xjzhv57qcVafzmRC/xAHp1AUbREgO4trmmajiHg8ZMOg1WJfKDy56qCcOXgE7CTQOYwRL/uzyzAWwWvhnmGcuw==",
+      "license": "ISC",
+      "dependencies": {
+        "chalk": "3.0.0",
+        "commander": "2.20.3",
+        "get-meta-file": "1.3.6",
+        "loop": "3.3.6",
+        "meta-exec": "1.4.4",
+        "mkdirp": "0.5.5",
+        "rimraf": "2.7.1",
+        "tildify": "^2.0.0"
+      },
+      "bin": {
+        "meta-project": "bin/meta-project",
+        "meta-project-add": "bin/meta-project-add",
+        "meta-project-create": "bin/meta-project-create",
+        "meta-project-import": "bin/meta-project-import",
+        "meta-project-migrate": "bin/meta-project-migrate"
+      }
+    },
+    "node_modules/meta-project/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/meta-project/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "license": "MIT"
+    },
+    "node_modules/meta-project/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/meta-project/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/meta-project/node_modules/rimraf": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      }
+    },
+    "node_modules/meta/node_modules/debug": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+      "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/meta/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "license": "MIT"
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.5"
+      },
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/mute-stream": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+      "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==",
+      "license": "ISC"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "deprecated": "Use your platform's native DOMException instead",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+      "license": "MIT",
+      "dependencies": {
+        "data-uri-to-buffer": "^4.0.0",
+        "fetch-blob": "^3.1.4",
+        "formdata-polyfill": "^4.0.10"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/node-fetch"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.27",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+      "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+      "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+      "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parse-passwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+      "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/prompt-sync": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz",
+      "integrity": "sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw==",
+      "license": "MIT",
+      "dependencies": {
+        "strip-ansi": "^5.0.0"
+      }
+    },
+    "node_modules/prompt-sync/node_modules/ansi-regex": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+      "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/prompt-sync/node_modules/strip-ansi": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/react": {
+      "version": "19.2.3",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
+      "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.2.3",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+      "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+      "license": "MIT",
+      "dependencies": {
+        "scheduler": "^0.27.0"
+      },
+      "peerDependencies": {
+        "react": "^19.2.3"
+      }
+    },
+    "node_modules/react-i18next": {
+      "version": "16.5.2",
+      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz",
+      "integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4",
+        "html-parse-stringify": "^3.0.1",
+        "use-sync-external-store": "^1.6.0"
+      },
+      "peerDependencies": {
+        "i18next": ">= 25.6.2",
+        "react": ">= 16.8.0",
+        "typescript": "^5"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-redux": {
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+      "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/use-sync-external-store": "^0.0.6",
+        "use-sync-external-store": "^1.4.0"
+      },
+      "peerDependencies": {
+        "@types/react": "^18.2.25 || ^19",
+        "react": "^18.0 || ^19",
+        "redux": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "redux": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-refresh": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+      "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-router": {
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
+      "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "^1.0.1",
+        "set-cookie-parser": "^2.6.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz",
+      "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==",
+      "license": "MIT",
+      "dependencies": {
+        "react-router": "7.11.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      }
+    },
+    "node_modules/redux": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+      "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+      "license": "MIT"
+    },
+    "node_modules/redux-thunk": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+      "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "redux": "^5.0.0"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+      "license": "ISC"
+    },
+    "node_modules/reselect": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+      "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+      "license": "MIT"
+    },
+    "node_modules/resolve-dir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+      "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
+      "license": "MIT",
+      "dependencies": {
+        "expand-tilde": "^2.0.0",
+        "global-modules": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-dir/node_modules/global-modules": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+      "license": "MIT",
+      "dependencies": {
+        "global-prefix": "^1.0.1",
+        "is-windows": "^1.0.1",
+        "resolve-dir": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-dir/node_modules/global-prefix": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+      "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
+      "license": "MIT",
+      "dependencies": {
+        "expand-tilde": "^2.0.2",
+        "homedir-polyfill": "^1.0.1",
+        "ini": "^1.3.4",
+        "is-windows": "^1.0.1",
+        "which": "^1.2.14"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-dir/node_modules/which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "which": "bin/which"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+      "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^2.0.0",
+        "signal-exit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/restore-cursor/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "license": "ISC"
+    },
+    "node_modules/rimraf": {
+      "version": "5.0.10",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
+      "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^10.3.7"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
+      "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.54.0",
+        "@rollup/rollup-android-arm64": "4.54.0",
+        "@rollup/rollup-darwin-arm64": "4.54.0",
+        "@rollup/rollup-darwin-x64": "4.54.0",
+        "@rollup/rollup-freebsd-arm64": "4.54.0",
+        "@rollup/rollup-freebsd-x64": "4.54.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.54.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.54.0",
+        "@rollup/rollup-linux-arm64-musl": "4.54.0",
+        "@rollup/rollup-linux-loong64-gnu": "4.54.0",
+        "@rollup/rollup-linux-ppc64-gnu": "4.54.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.54.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.54.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.54.0",
+        "@rollup/rollup-linux-x64-gnu": "4.54.0",
+        "@rollup/rollup-linux-x64-musl": "4.54.0",
+        "@rollup/rollup-openharmony-arm64": "4.54.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.54.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.54.0",
+        "@rollup/rollup-win32-x64-gnu": "4.54.0",
+        "@rollup/rollup-win32-x64-msvc": "4.54.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-async": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+      "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/rxjs": {
+      "version": "6.6.7",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+      "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^1.9.0"
+      },
+      "engines": {
+        "npm": ">=2.0.0"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/scheduler": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+      "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+      "license": "ISC"
+    },
+    "node_modules/set-cookie-parser": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+      "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+      "license": "MIT"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/should": {
+      "version": "13.2.3",
+      "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz",
+      "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==",
+      "license": "MIT",
+      "dependencies": {
+        "should-equal": "^2.0.0",
+        "should-format": "^3.0.3",
+        "should-type": "^1.4.0",
+        "should-type-adaptors": "^1.0.1",
+        "should-util": "^1.0.0"
+      }
+    },
+    "node_modules/should-equal": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz",
+      "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==",
+      "license": "MIT",
+      "dependencies": {
+        "should-type": "^1.4.0"
+      }
+    },
+    "node_modules/should-format": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
+      "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==",
+      "license": "MIT",
+      "dependencies": {
+        "should-type": "^1.3.0",
+        "should-type-adaptors": "^1.0.1"
+      }
+    },
+    "node_modules/should-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
+      "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==",
+      "license": "MIT"
+    },
+    "node_modules/should-type-adaptors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz",
+      "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==",
+      "license": "MIT",
+      "dependencies": {
+        "should-type": "^1.3.0",
+        "should-util": "^1.0.0"
+      }
+    },
+    "node_modules/should-util": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz",
+      "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==",
+      "license": "MIT"
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tabtab": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/tabtab/-/tabtab-3.0.2.tgz",
+      "integrity": "sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.1",
+        "es6-promisify": "^6.0.0",
+        "inquirer": "^6.0.0",
+        "minimist": "^1.2.0",
+        "mkdirp": "^0.5.1",
+        "untildify": "^3.0.3"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "4.1.18",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
+      "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
+      "license": "MIT"
+    },
+    "node_modules/tapable": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+      "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+      "license": "MIT"
+    },
+    "node_modules/tildify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz",
+      "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "license": "MIT",
+      "dependencies": {
+        "os-tmpdir": "~1.0.2"
+      },
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+      "license": "0BSD"
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/untildify": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz",
+      "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/use-sync-external-store": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+      "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+      "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/void-elements": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+      "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/web-streams-polyfill": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+      "license": "ISC"
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+      "license": "ISC"
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yargs": {
+      "version": "14.2.3",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
+      "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^5.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^3.0.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^3.0.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^15.0.1"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "15.0.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.3.tgz",
+      "integrity": "sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==",
+      "license": "ISC",
+      "dependencies": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      }
+    },
+    "node_modules/yargs/node_modules/ansi-regex": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+      "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/yargs/node_modules/emoji-regex": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+      "license": "MIT"
+    },
+    "node_modules/yargs/node_modules/is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+      "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^7.0.1",
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/yargs/node_modules/strip-ansi": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    }
+  }
+}

+ 34 - 0
EsimLao/esim-vite/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "esim-vite",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "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",
+    "crypto-js": "^4.2.0",
+    "i18next": "^25.7.4",
+    "meta": "^2.2.25",
+    "react": "^19.2.3",
+    "react-dom": "^19.2.3",
+    "react-i18next": "^16.5.2",
+    "react-redux": "^9.2.0",
+    "react-router-dom": "^7.11.0",
+    "redux": "^5.0.1",
+    "tailwindcss": "^4.1.18"
+  },
+  "devDependencies": {
+    "@types/node": "^22.14.0",
+    "@vitejs/plugin-react": "^5.0.0",
+    "typescript": "~5.8.2",
+    "vite": "^6.2.0"
+  }
+}

+ 95 - 0
EsimLao/esim-vite/src/App.tsx

@@ -0,0 +1,95 @@
+import React from "react";
+import { Routes, Route, useLocation, Navigate } from "react-router-dom";
+import Header from "./components/Header";
+import Footer from "./components/Footer";
+import HomeView from "./pages/home/HomeView";
+import Sidebar from "./components/Sidebar";
+import ProductDetailView from "./pages/product-detail/ProductDetailView";
+import BuySimView from "./pages/buy-sim/BuySimView";
+import SupportView from "./pages/support/SupportView";
+import NewsView from "./pages/news/NewsView";
+import ArticleDetailView from "./pages/news/NewsDetailView";
+import LoginView from "./pages/login/LoginView";
+import ContactView from "./pages/contact/ContactView";
+import TopLoader from "./components/TopLoader";
+import CheckoutView from "./pages/checkout/CheckoutView";
+import Popup from "./components/Popup";
+import OrderHistoryView from "./pages/order-history/OrderHistoryView";
+import OrderDetailView from "./pages/order-detail/OrderDetailView";
+import CompatibilityModal from "./components/CompatibilityModal";
+import QRCodeModal from "./components/QRCodeModal";
+import { useAppSelector } from "./hooks/useRedux";
+import ScrollToTop from "./components/ScrollToTop";
+import TermsView from "./pages/terms/TermsView";
+
+const App: React.FC = () => {
+  const location = useLocation();
+  const loading = useAppSelector((state) => state.loading);
+  const path = location.pathname;
+
+  const isAiView = path.startsWith("/ai");
+  const isPlainView = path === "/login";
+
+  // Helper to determine if footer should be shown
+  const showFooter = !isAiView && !isPlainView;
+
+  return (
+    <div
+      className={`flex flex-col min-h-screen bg-white selection:bg-red-100 ${
+        isAiView ? "h-screen overflow-hidden" : ""
+      }`}
+    >
+      {!isPlainView && <Header />}
+      <ScrollToTop />
+      <Popup />
+      <CompatibilityModal />
+      <QRCodeModal />
+
+      <div className={`flex flex-1 ${isAiView ? "overflow-hidden" : ""}`}>
+        {isAiView && <Sidebar />}
+
+        <main
+          className={`flex-1 relative flex flex-col ${
+            isAiView ? "overflow-y-auto bg-slate-50" : "bg-white"
+          }`}
+        >
+          <TopLoader />
+
+          <div className="flex-1">
+            <Routes>
+              <Route path="/" element={<HomeView />} />
+              <Route path="/buy-sim" element={<BuySimView />} />
+              <Route
+                path="/product-detail/:id"
+                element={<ProductDetailView />}
+              />
+              <Route path="/checkout" element={<CheckoutView />} />
+              <Route path="/support" element={<SupportView />} />
+              <Route path="/news" element={<NewsView />} />
+              <Route path="/news/:id" element={<ArticleDetailView />} />
+              <Route path="/contact" element={<ContactView />} />
+              <Route path="/login" element={<LoginView />} />
+              <Route path="/order-history" element={<OrderHistoryView />} />
+              <Route path="/terms" element={<TermsView />} />
+
+              <Route
+                path="/order-history-detail/:id"
+                element={<OrderDetailView />}
+              />
+              {/* <Route
+                path="/order-history-detail"
+                element={<OrderDetailView />}
+              /> */}
+              {/* Fallback */}
+              <Route path="*" element={<Navigate to="/" replace />} />
+            </Routes>
+          </div>
+
+          {showFooter && <Footer />}
+        </main>
+      </div>
+    </div>
+  );
+};
+
+export default App;

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

@@ -0,0 +1,37 @@
+import {
+  LoadArticleDetailResponse,
+  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<LoadArticleDetailResponse>("/detail", {
+      id: articleId,
+    });
+  }
+}
+
+export const articleApi = new ArticleApi();

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

@@ -0,0 +1,45 @@
+import {
+  AccountInfo,
+  GoogleCallbackResponse,
+  GoogleLoginResponse,
+  RequestOtpResponse,
+} from "../services/auth/types";
+import { Area } from "../services/product/type";
+import { BaseApi } from "./baseApi";
+
+class AuthApi extends BaseApi {
+  constructor() {
+    super("/auth");
+  }
+
+  async requestOtp({ email }) {
+    return this.authPost<RequestOtpResponse>("/request-otp", { email });
+  }
+
+  async verifyOtp({ email, otp }) {
+    return this.authPost<AccountInfo>("/verify-otp", { email, otpCode: otp });
+  }
+
+  async resendOtp({ email }) {
+    return this.authPost<RequestOtpResponse>("/resend-otp", { email });
+  }
+
+  async googleLogin() {
+    return this.authPost<GoogleLoginResponse>("/google-login", {});
+  }
+
+  async googleCallback({ code }) {
+    return this.authPost<AccountInfo>("/google-callback", {
+      code,
+    });
+  }
+
+  async partnerLogin({ partner, data }) {
+    return this.authPost<AccountInfo>("/partner-login", {
+      partner,
+      data,
+    });
+  }
+}
+
+export const authApi = new AuthApi();

+ 40 - 0
EsimLao/esim-vite/src/apis/axios.ts

@@ -0,0 +1,40 @@
+import axios, { AxiosInstance } from "axios";
+import { getWithExpiry } from "../logic/loigicUtils";
+
+export const createAxiosInstance = (baseURL: string): AxiosInstance => {
+  const instance = axios.create({
+    baseURL,
+    timeout: 10000,
+    headers: {
+      "Content-Type": "application/json",
+    },
+  });
+
+  instance.interceptors.request.use((config) => {
+    const token = getWithExpiry("token");
+    const lang = getWithExpiry("selectedLanguage") || "en";
+    config.headers["Content-Type"] = "application/json";
+    if (lang) {
+      config.headers["Accept-Language"] = lang;
+    }
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  });
+
+  instance.interceptors.response.use(
+    (response) => response.data,
+    (error) => {
+      if (error.response?.status === 401) {
+        localStorage.removeItem("token");
+        localStorage.removeItem("refreshToken");
+        localStorage.removeItem("accountInfo");
+        window.location.href = "/login";
+      }
+      return Promise.reject(error);
+    },
+  );
+
+  return instance;
+};

+ 61 - 0
EsimLao/esim-vite/src/apis/baseApi.ts

@@ -0,0 +1,61 @@
+import { createAxiosInstance } from "./axios";
+
+export type Response<T> = {
+  errorCode: string;
+  message: string;
+  msg: string;
+  status: boolean;
+  data: T;
+};
+
+export const authApiInstance = createAxiosInstance(
+  import.meta.env.VITE_API_AUTH_URL
+);
+
+export const productApiInstance = createAxiosInstance(
+  import.meta.env.VITE_API_PRODUCT_URL
+);
+
+export class BaseApi {
+  protected baseUrl: string;
+  protected defaultTimeout = 10000; // 10 giây
+
+  constructor(baseUrl: string, timeout?: number) {
+    this.baseUrl = baseUrl;
+    if (timeout) {
+      this.defaultTimeout = timeout;
+    }
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  async productPost<T>(
+    endpoint: string = "",
+    data?: any,
+    timeout?: number
+  ): Promise<Response<T>> {
+    return productApiInstance.post(`${this.baseUrl}${endpoint}`, data, {
+      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>> {
+    return authApiInstance.post(`${this.baseUrl}${endpoint}`, data, {
+      timeout: timeout ?? this.defaultTimeout,
+    });
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  async authGet<T>(
+    endpoint: string = "",
+    timeout?: number
+  ): Promise<Response<T>> {
+    return authApiInstance.get(`${this.baseUrl}${endpoint}`, {
+      timeout: timeout ?? this.defaultTimeout,
+    });
+  }
+}

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

@@ -0,0 +1,71 @@
+import {
+  LoadArticleResponse,
+  LoadCategoryResponse,
+} from "../services/article/types";
+import {
+  LoadBannerResponse,
+  LoadDeviceMetaResponse,
+  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 }) {
+    return this.authPost<LoadFaqResponse>("/faq", {
+      pageNumber,
+      pageSize,
+      categoryId,
+      isFeatured,
+    });
+  }
+
+  // async LoadFaqGuide({
+  //   pageNumber,
+  //   pageSize,
+  //   categoryId = 1,
+  //   isFeatured = false,
+  // }) {
+  //   return this.authPost<LoadFaqResponse>("/faq", {
+  //     pageNumber,
+  //     pageSize,
+  //     categoryId,
+  //     isFeatured,
+  //   });
+  // }
+
+  async LoadDeviceMetaData() {
+    return this.authGet<LoadDeviceMetaResponse>("/device-metadata");
+  }
+}
+
+export const contentApi = new ContentApi();

+ 72 - 0
EsimLao/esim-vite/src/apis/productApi.ts

@@ -0,0 +1,72 @@
+import {
+  Area,
+  CheckoutDetailResponse,
+  ConfirmOrderResponse,
+} from "../services/product/type";
+import { BaseApi } from "./baseApi";
+
+class ProductApi extends BaseApi {
+  constructor() {
+    super("/api");
+  }
+
+  async loadArea({ isCountry = "-1", isPopular = "-1" }) {
+    return this.productPost("/list_area", {
+      isCountry,
+      isPopular,
+    });
+  }
+
+  async loadPackage({ areaId, dataType = "-1" }) {
+    return this.productPost("/list_packg", { areaId, dataType });
+  }
+
+  async checkout({ packgId, quantity }) {
+    return this.productPost<CheckoutDetailResponse>("/place_order", {
+      packgId,
+      quantity,
+    });
+  }
+
+  async confirmOrder({
+    surName,
+    lastName,
+    email,
+    phoneNumber,
+    paymentChannelId,
+    packgId,
+    quantity,
+  }) {
+    return this.productPost<ConfirmOrderResponse>("/confirm_order", {
+      surName,
+      lastName,
+      email,
+      phoneNumber,
+      paymentChannelId,
+      packgId,
+      quantity,
+    });
+  }
+
+  async loadPaymentChannel() {
+    return this.productPost("/list_payment_channel", {});
+  }
+
+  async getOrderHistory({ searchOrder, fromDate, toDate, status }) {
+    return this.productPost("/list_order", { searchOrder, fromDate, toDate, status });
+  }
+
+  async getOrderDetail({ orderId }) {
+    return this.productPost("/get_order_detail", { orderId });
+  }
+
+  async checkDataUsage({ iccid }) {
+    return this.productPost("/check_data_usage", { iccid });
+  }
+
+  async loadRelatedArea({ areaId }) {
+    return this.productPost("/related_area", { areaId });
+  }
+}
+
+export const productApi = new ProductApi();

+ 17 - 0
EsimLao/esim-vite/src/app/store.ts

@@ -0,0 +1,17 @@
+import { configureStore } from "@reduxjs/toolkit";
+import loadingReducer from "../features/loading/loadingSlice";
+import areasReducer from "../features/areas/areasSlice";
+import popupReducer from "../features/popup/popupSlice";
+import accountReducer from "../features/account/accuntSlice";
+
+export const store = configureStore({
+  reducer: {
+    loading: loadingReducer,
+    areas: areasReducer,
+    popup: popupReducer,
+    account: accountReducer,
+  },
+});
+
+export type RootState = ReturnType<typeof store.getState>;
+export type AppDispatch = typeof store.dispatch;

+ 78 - 0
EsimLao/esim-vite/src/assets/img/getgo.svg

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3006.17 1200">
+  <!-- Generator: Adobe Illustrator 29.8.3, SVG Export Plug-In . SVG Version: 2.1.1 Build 3)  -->
+  <defs>
+    <style>
+      .st0 {
+        fill: url(#linear-gradient2);
+      }
+
+      .st1 {
+        fill: #ed2024;
+      }
+
+      .st2 {
+        fill: url(#linear-gradient1);
+      }
+
+      .st3 {
+        fill: #e61e25;
+      }
+
+      .st4 {
+        fill: url(#linear-gradient);
+      }
+    </style>
+    <linearGradient id="linear-gradient" x1="2456.77" y1="855.18" x2="2951.25" y2="1129.08" gradientTransform="translate(0 1199.28) scale(1 -1)" gradientUnits="userSpaceOnUse">
+      <stop offset="0" stop-color="#ec1d24"/>
+      <stop offset="1" stop-color="#ee3728"/>
+    </linearGradient>
+    <linearGradient id="linear-gradient1" x1="2036.44" y1="774.51" x2="2420" y2="894.85" gradientTransform="translate(0 1199.28) scale(1 -1)" gradientUnits="userSpaceOnUse">
+      <stop offset=".08" stop-color="#fff"/>
+      <stop offset=".32" stop-color="#f9beaa"/>
+      <stop offset=".56" stop-color="#f47c53"/>
+      <stop offset="1" stop-color="#af1f23"/>
+    </linearGradient>
+    <linearGradient id="linear-gradient2" x1="2265.3" y1="816.43" x2="2455.38" y2="816.43" gradientTransform="translate(0 1199.28) scale(1 -1)" gradientUnits="userSpaceOnUse">
+      <stop offset=".05" stop-color="#fff"/>
+      <stop offset=".56" stop-color="#f47c53"/>
+      <stop offset="1" stop-color="#af1f23"/>
+    </linearGradient>
+  </defs>
+  <g>
+    <path class="st3" d="M169.34,1091.13h34.48v-91.92h-28.47v-15.94c15.3-2.55,25.86-6.16,35.54-11.53h20.82v119.39h30.31v20.69h-92.69v-20.69h.01Z"/>
+    <path class="st3" d="M285.89,1047.11h57.53v18.49h-57.53v-18.49Z"/>
+    <path class="st3" d="M365.41,1040.33c0-47.1,32.44-75.1,72.54-75.1,19.85,0,35.64,8.63,45.62,18.22l-15.01,16.46c-8.14-7.4-17.63-12.5-30.02-12.5-25.76,0-44.26,19.81-44.26,52.12s17.05,52.74,43.58,52.74c14.04,0,24.89-5.9,33.8-14.79l15.01,16.02c-12.83,13.56-29.35,20.96-49.88,20.96-39.81,0-71.38-26.5-71.38-74.13h0Z"/>
+    <path class="st3" d="M510.77,1084v-127.66h27.89v128.98c0,6.25,3.1,8.45,6,8.45,1.26,0,2.23,0,4.36-.44l3.49,18.93c-3.39,1.23-8.04,2.2-14.72,2.2-19.76,0-27.02-11.62-27.02-30.46Z"/>
+    <path class="st3" d="M573.19,970.42c0-8.54,7.36-14.35,17.24-14.35s17.24,5.81,17.24,14.35-7.36,14.35-17.24,14.35-17.24-5.99-17.24-14.35ZM576.39,1003.7h27.89v108.12h-27.89v-108.12h0Z"/>
+    <path class="st3" d="M631.98,1057.85c0-36.01,27.99-56.79,59.66-56.79,15.11,0,26.15,5.28,34.77,12.15l-13.56,16.46c-6.3-5.02-12.3-7.92-19.76-7.92-19.18,0-32.44,14.44-32.44,36.1s13.07,35.92,31.57,35.92c9.3,0,17.82-4.14,24.6-9.33l11.33,16.73c-11.14,8.98-25.28,13.29-38.84,13.29-32.44,0-57.34-20.78-57.34-56.61h.01Z"/>
+    <path class="st3" d="M751.68,956.34h27.31v97.11h.77l44.07-49.74h30.51l-40.77,44.02,44.94,64.1h-30.31l-30.6-46.93-18.6,19.46v27.47h-27.31v-155.48h-.01Z"/>
+    <path class="st3" d="M960.87,989.35h-45.71v-21.48h119.51v21.48h-45.71v122.47h-28.09v-122.47h0Z"/>
+    <path class="st3" d="M1033.65,1057.85c0-36.01,26.92-56.79,56.56-56.79s56.56,20.78,56.56,56.79-26.92,56.61-56.56,56.61-56.56-20.78-56.56-56.61ZM1118.11,1057.85c0-21.66-10.46-36.1-27.89-36.1s-27.89,14.44-27.89,36.1,10.46,35.92,27.89,35.92,27.89-14.26,27.89-35.92Z"/>
+    <path class="st3" d="M1255.78,989.35h-45.71v-21.48h119.51v21.48h-45.71v122.47h-28.09v-122.47h0Z"/>
+    <path class="st3" d="M1353.59,956.34h27.89v40.24l-1.16,20.87c9.98-8.54,21.79-16.38,38.06-16.38,25.47,0,36.61,15.67,36.61,42.97v67.79h-27.89v-64.54c0-17.78-5.62-24.48-18.5-24.48-10.46,0-17.34,4.67-27.12,13.47v75.54h-27.89v-155.48h0Z"/>
+    <path class="st3" d="M1480.85,1057.85c0-35.13,26.83-56.79,54.82-56.79,32.16,0,49.1,21.13,49.1,51.33,0,4.84-.58,9.68-1.21,12.33h-75.5c2.32,19.19,15.88,30.11,35.06,30.11,10.17,0,18.89-2.91,27.6-7.84l9.49,15.76c-11.33,6.96-25.67,11.71-40.77,11.71-32.83,0-58.59-20.96-58.59-56.61ZM1560.66,1047.46c0-16.64-8.13-26.77-24.41-26.77-13.85,0-26.05,9.42-28.47,26.77h52.88Z"/>
+    <path class="st3" d="M1646.51,967.87h28.86l13.75,72.72c2.57,15.32,5.28,30.64,7.84,45.96h.97c3.29-15.32,6.78-30.77,10.27-45.96l19.47-72.72h24.41l19.47,72.72c3.49,14.92,6.97,30.51,10.46,45.96h.97c2.57-15.45,5.08-30.9,7.65-45.96l13.75-72.72h26.92l-30.41,143.95h-34.77l-19.66-75.8c-2.61-11.18-4.94-22.19-6.97-33.1h-.97c-2.18,10.92-4.55,21.92-7.17,33.1l-19.18,75.8h-34.19l-31.48-143.95h.01Z"/>
+    <path class="st3" d="M1845.73,1057.85c0-36.01,26.92-56.79,56.56-56.79s56.56,20.78,56.56,56.79-26.92,56.61-56.56,56.61-56.56-20.78-56.56-56.61ZM1930.18,1057.85c0-21.66-10.46-36.1-27.89-36.1s-27.89,14.44-27.89,36.1,10.46,35.92,27.89,35.92,27.89-14.26,27.89-35.92Z"/>
+    <path class="st3" d="M1986.5,1003.7h22.86l2.13,19.28h.78c8.47-14.09,21.06-21.92,33.8-21.92,6.05,0,9.88.71,13.46,2.2l-4.84,22.01c-4.16-1.06-7.26-1.76-12.2-1.76-9.49,0-20.92,5.9-28.09,22.36v65.94h-27.89v-108.12h-.01Z"/>
+    <path class="st3" d="M2076.81,1084v-127.66h27.89v128.98c0,6.25,3.1,8.45,6,8.45,1.26,0,2.23,0,4.36-.44l3.49,18.93c-3.39,1.23-8.04,2.2-14.72,2.2-19.76,0-27.02-11.62-27.02-30.46Z"/>
+    <path class="st3" d="M2135.16,1057.85c0-35.22,24.31-56.79,50.36-56.79,13.46,0,21.99,4.58,31.19,11.89l-1.07-17.43v-39.18h27.89v155.48h-22.86l-2.13-11.71h-.78c-8.81,8.01-21.02,14.35-33.8,14.35-29.64,0-48.81-20.96-48.81-56.61h.01ZM2215.64,1081.36v-50.36c-8.13-6.6-16.17-8.98-24.21-8.98-15.11,0-27.6,13.03-27.6,35.57s9.88,35.92,27.02,35.92c9.2,0,16.95-3.7,24.79-12.15Z"/>
+  </g>
+  <rect class="st1" x="1925.92" y="351.5" width="103.56" height="479.79" rx="25.77" ry="25.77"/>
+  <g>
+    <path class="st1" d="M883.19,487.3c-17.87,18.98-37.46,36.97-55.04,56.28-2.99,3.28-15.69,17.34-16.82,20.1-.77,1.87-.97,3.6-.37,5.55l63.61,74.62c41.2,48.68,87.22,95.56,127.12,145.09,4.17,5.18,18.86,21.22,12.44,27.68-1.64,1.66-12.12,3.24-15.01,3.45-23.76,1.75-66.78,3.38-89.21-1.61-15.19-3.38-29.16-22.49-39.57-34.29-41.16-46.68-79.17-96.69-121.31-142.47-12.92-14.04-12.82-5.15-22.67,4.79-5.73,5.79-33.49,30-34.62,35.01-2.58,38.1,2.66,79.2-.18,116.94-1.17,15.45-7.11,21.46-22.69,22.68-12.93,1.01-53.9,1.59-64.97-1.58-9.25-2.65-11.95-8.93-12.66-17.94v-413.69c1.84-11.74,2.05-23.85,14.25-28.99,10.82-4.56,45.45-5.12,58.1-4.24,17.07,1.19,26.41,8.1,27.95,25.86,3.33,38.28-.62,85.57-1.14,124.5-.16,12.42-1.02,28.13,0,40.18.25,2.95.16,6.11,3.74,6.42,1.49-.14,4.97-2.76,6.31-3.85,14.44-11.77,29.69-32.8,42.71-46.95,31.56-34.27,63.3-68.19,95.55-101.78,15.37-16.01,27.83-36.42,50.71-42.14,16.45-4.12,68.72-5.97,83.48.78,6.54,2.99,11.03,9.89,9.33,17.08-2.46,10.41-31.32,40.02-40.11,49.61-19.35,21.12-39.29,42.05-58.94,62.92h0Z"/>
+    <path class="st1" d="M1590.54,454.2c-12.44,11.47-16.47,29.97-9.21,45.05,10.41,21.64,33.16,28.18,55.04,33.99,86.24,22.89,221.94,23.86,233.39,137.45,18.67,185.16-245.88,185.88-361.91,123.18-14.82-8.01-44.8-26.85-44.21-45.46.29-9.17,15.22-39.05,22.57-45.15,17.17-14.25,52.91,11.11,69.48,18.84,54.65,25.49,128.38,42.04,185.73,15.19,47.07-22.03,40.21-73.06-6.73-91.43-75.56-29.57-208.29-18.22-245.83-105.58-14.78-34.4-13.49-79.26,5.11-111.98,54.41-95.73,219.11-88.75,307.94-50.21,15.51,6.73,43.06,19.23,44.66,37.97,1.17,13.74-13.53,46.25-29.28,48.91-15.25,2.58-45.01-12.5-60.66-17.63-46.05-15.1-126.97-29.24-166.09,6.86h0Z"/>
+    <path class="st1" d="M485.73,460.75c-15.8,13.01-54.09-8.26-71.17-13.88-46.81-15.4-118.12-27.72-160.53,3.43-18.1,13.3-23.23,40-8.51,57.85,21.55,26.12,106.4,35.58,140.52,43.28,67.29,15.18,139.32,42.97,143.32,121.27,4.06,79.53-33.7,129.61-112.49,148.5-83.12,19.92-188.66,10.19-262.04-34.68-10.73-6.56-29.22-19.84-30.68-33.03s11.96-43.14,23.28-50.65c18.45-12.25,46.56,9.68,63.25,17.51,28.57,13.41,62.96,25.03,94.55,28.35,45.68,4.79,133.49-.88,126.99-64.55-3.23-31.64-51.31-43.68-77.01-49.7-84.46-19.77-209-23.57-216.35-134.51-8.71-131.53,138.64-161.18,243.08-144.27,27.57,4.46,103.89,24.55,119.35,47.49,8.89,13.2.8,35.43-7.4,47.51-.76,1.12-7.61,9.61-8.18,10.08h.02Z"/>
+    <path class="st1" d="M1353.8,369.86c8.81-9.13,18.6-12.94,31.02-14.88,14.82-2.31,48.81-2.42,62.74,2.15,17.38,5.7,13.61,24.76,4.66,36.76-46.25,69.81-95.58,137.74-141.45,207.79-13.29,20.29-23.3,29.69-22.8,55.37.44,22.2.15,44.24,0,66.38-.17,24.23,2.24,53.66.09,77.11-1.25,13.62-8.74,18.95-21.69,20.51-13.93,1.68-27.32.73-41.09-.03-18.26-1-34.8,2.48-37.49-21.6-3.45-50.63,4.56-107.33.03-157.24-1.27-14.01-14.89-28.48-22.43-39.82-47.54-71.52-96.27-142.25-144.13-213.55-5.82-8.5-10.08-23.16.61-29.62,12.36-7.47,55.51-6.73,70.14-4.2,29.88,5.15,36.87,23.11,52.68,45.44,31.37,44.31,61.37,89.62,90.1,135.68,5.09,6.82,12.43-7.45,15.48-11.83,13.63-19.58,25.92-40.16,39.19-59.99,18.8-28.09,39.09-62.18,59.29-88.42,1.29-1.68,3.65-4.57,5.05-6.03v.02Z"/>
+  </g>
+  <path class="st1" d="M2233.11,807.24c-.51,3.96-5.62,16.18-8.52,18.79-3.69,3.31-14.51,4.7-19.45,5.07-14.34,1.05-37.18.1-51.73-1.05-14.95-1.19-20.54-6.78-21.63-22.75v-323.08c.52-6.3,1.66-11.47,7.5-14.31,32.02-11.58,65.44-20.42,96.32-35.2,7.3-3.49,16.45-12.5,24.11-8.68l110.55,137.06c4.47,5.54,8.59,12.78,13.19,18.3,3.24,3.88,13.36,16.26,17.89,15.57l2.73-1.68c13.81-17.74,26.46-36.52,40.09-54.39,38.2-50.07,75.95-100.49,114.95-149.9,7.41-9.39,18.22-28.06,28.62-33.16,11.91-5.83,42.05-6.6,55.29-5.11,9.45,1.06,18.51,10,18.51,19.64,0,51.17.28,91.65,0,137.37l-.06,300.84c-1.36,12.04-5.54,18.17-17.41,19.42-15.24,1.61-49.01,1.58-64.22-.13-9.61-1.08-14.74-5.4-17.06-15.35l-1.35-260.53c-.33-3.73-3.08-5.43-5.32-1.88-11.91,18.63-26.79,34.8-40.2,52.05-24.48,31.5-49.27,69.04-75.45,98.22-22.46,25.03-49.92,36.99-77.03,9.97-28.99-28.89-58.73-73.99-84.96-107.08-11.9-15.02-26-34.65-38.48-48.32-1.89-2.07-3.7-4.98-6.87-4.43v254.73h-.01Z"/>
+  <path class="st4" d="M2804.61,213.96c1.36,1.46,4.41,6.77,4.54,8.66,1.34,20.13-40.05,21.62-44,38.19-.74,3.1-2.86,17.22-1.85,19.26,2.18,4.41,23.9-.34,25.79,11.14,1.5,9.11-2.08,11.69-9.11,16.08-7.05,4.41-24.01,9.78-27.62,16.69-1.26,2.4-1.58,5.75-2.09,8.46-6.92,36.42-1.07,77.47-39.54,98.68-10.13,5.59-23.42,6.75-23.95-8.14-.41-11.37,3.31-26.25,4.25-37.93,3.44-42.73,6.42-87.02,2.1-129.76-.42-4.16-1.67-18.35-5.26-20.06-5.86-2.78-16.23,2.22-21.82,4.56-41.76,17.48-83.09,43.73-125.19,61.57-13.19,10.13-9.51,33.98-14.56,48.75-6.62,8.95-35.1,29.85-41.09,11.8-4.21-12.68-5.47-31.1-8.89-44.92-2.23-9.02-2.22-13.45-9.53-20.01-14.43-12.94-31.64-23.18-45.88-36.42-9.53-8.86-15.62-14.7.35-21.83,12.87-5.75,27.04-7.23,40.85-4.59,23.5,4.49,30.17,14.76,56.69,7.17,22.44-6.42,80.4-34.87,98.29-49.43,15.89-12.93,13.82-22.87-4.91-30.79-43.09-15.08-94.58-21.97-136.62-38.53-14.74-5.81-8.66-10.61,1.99-17.47,36.9-23.8,76.45-4.03,115.9-2.29,3.48.15,8.15.38,11.5-.08,10.81-1.48,24.47-19.33,33.78-25.31,7.11-2.51,12.57-1.42,15.58,5.68,3.24,7.63-1.5,23.98,2.18,27.9,6.97,7.41,32.05-4.7,39.86-8.66,10.88-5.51,36.92-26.76,45.91-8.02,3.07,6.4-1.35,23.73,6.18,23.29,5.74-.34,34.78-24.67,41.27-29.54,34.3-25.71,58.15-46.52,102.62-53.53,26.67-4.21,49.47-3.61,32.05,29.94-22.73,43.79-89.56,73.94-128.8,102.26-9.03,6.52-15.76,11.03-16.97,23.13-.29,2.85-.49,13.24.51,15.29,1.67,3.44,8.11,1.66,11.5,2.22,4.65.76,10.75,3.1,13.99,6.58h0Z"/>
+  <path class="st2" d="M2091.92,361.74l-12.98.84c-9.35.6-16.95,7.73-18.17,17.02-2.21,16.84-4.8,42.72-2.09,56.14.7,3.44,2.45,6.59,5.03,8.96,6.14,5.66,20.93,14.87,46.79,6.25,35.54-11.85,266-89.85,296.2-117.29,0,0-202.58,60.21-240.45,60.41,0,0-13.01-5.61-8.36-15.13,0,0,7.2-3.02,23-5.58s214.2-48.55,240.45-68.53c0,0-268.09,60.17-329.43,56.92h.01Z"/>
+  <path class="st0" d="M2266.19,430.79l.25.5c1.62,3.25,5.46,4.72,8.84,3.37,25.13-10.01,123.49-51.19,180.1-104.12,0,0-140.68,71.87-184.66,88.62-4.66,1.77-6.76,7.17-4.53,11.63h0Z"/>
+  <g>
+    <path class="st1" d="M1963.84,316.07c-5.6,0-10.16-5.01-10.16-11.16v-26.93c0-6.15,4.56-11.16,10.16-11.16s10.16,5.01,10.16,11.16v26.93c0,6.15-4.56,11.16-10.16,11.16Z"/>
+    <path class="st1" d="M2019.33,316.07c-5.6,0-10.16-5.01-10.16-11.16v-65.46c0-6.15,4.56-11.16,10.16-11.16s10.16,5.01,10.16,11.16v65.46c0,6.15-4.56,11.16-10.16,11.16Z"/>
+    <path class="st1" d="M2001.42,306.11c-.07,6-5.23,9.25-10.06,9.25-3.22,0-6.03-1.4-7.71-3.87-1.04-1.36-1.71-3.06-2.04-5.27-.07-.44-.07-.88-.07-1.4v-46.12c0-1.51.28-2.97.79-4.31,1.09-2.85,3.36-4.83,5.96-5.83,2.51-.97,5.52-.94,7.92.38,1.16.64,2.21,1.52,3.06,2.6,1.53,1.92,2.41,4.45,2.24,7.02.17,17.46.13,33-.1,47.55h0Z"/>
+    <path class="st1" d="M1936.09,285.76h0c5.61,0,10.16,4.55,10.16,10.16v9.99c0,5.61-4.55,10.16-10.16,10.16h0c-5.61,0-10.16-4.55-10.16-10.16v-9.99c0-5.61,4.55-10.16,10.16-10.16Z"/>
+  </g>
+</svg>

Різницю між файлами не показано, бо вона завелика
+ 65 - 0
EsimLao/esim-vite/src/assets/img/getgo1.svg


Різницю між файлами не показано, бо вона завелика
+ 65 - 0
EsimLao/esim-vite/src/assets/img/getgo_gradient.svg


BIN
EsimLao/esim-vite/src/assets/img/logo-skysim.png


BIN
EsimLao/esim-vite/src/assets/img/logo.png


BIN
EsimLao/esim-vite/src/assets/img/logo1.png


BIN
EsimLao/esim-vite/src/assets/img/logo2.png


BIN
EsimLao/esim-vite/src/assets/img/logo_color.png


BIN
EsimLao/esim-vite/src/assets/img/logo_white.png


BIN
EsimLao/esim-vite/src/assets/img/partner1.png


BIN
EsimLao/esim-vite/src/assets/img/partner2.png


+ 176 - 0
EsimLao/esim-vite/src/components/ChatView.tsx

@@ -0,0 +1,176 @@
+import React, { useState, useRef, useEffect } from "react";
+import { ChatMessage } from "../services/types";
+import { chatStream } from "../services/gemini";
+
+const ChatView: React.FC = () => {
+  const [messages, setMessages] = useState<ChatMessage[]>([]);
+  const [input, setInput] = useState("");
+  const [isTyping, setIsTyping] = useState(false);
+  const scrollRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (scrollRef.current) {
+      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+    }
+  }, [messages, isTyping]);
+
+  const handleSend = async () => {
+    if (!input.trim() || isTyping) return;
+
+    const userMsg: ChatMessage = {
+      role: "user",
+      text: input,
+      timestamp: new Date(),
+    };
+    setMessages((prev) => [...prev, userMsg]);
+    setInput("");
+    setIsTyping(true);
+
+    let modelResponse = "";
+    const modelMsg: ChatMessage = {
+      role: "model",
+      text: "",
+      timestamp: new Date(),
+    };
+    setMessages((prev) => [...prev, modelMsg]);
+
+    try {
+      await chatStream([...messages, userMsg], (chunk) => {
+        modelResponse += chunk;
+        setMessages((prev) => {
+          const updated = [...prev];
+          updated[updated.length - 1] = {
+            ...updated[updated.length - 1],
+            text: modelResponse,
+          };
+          return updated;
+        });
+      });
+    } catch (error) {
+      console.error(error);
+      setMessages((prev) => {
+        const updated = [...prev];
+        updated[updated.length - 1] = {
+          ...updated[updated.length - 1],
+          text: "Connectivity Error: Unable to reach the processing unit.",
+        };
+        return updated;
+      });
+    } finally {
+      setIsTyping(false);
+    }
+  };
+
+  return (
+    <div className="flex flex-col h-full bg-slate-50/50">
+      <div ref={scrollRef} className="flex-1 overflow-y-auto p-10 space-y-8">
+        {messages.length === 0 && (
+          <div className="h-full flex flex-col items-center justify-center text-center space-y-6">
+            <div className="w-20 h-20 bg-white rounded-3xl flex items-center justify-center shadow-2xl border border-slate-100">
+              <svg
+                className="w-10 h-10 text-[#004ea8]"
+                fill="none"
+                stroke="currentColor"
+                viewBox="0 0 24 24"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
+                />
+              </svg>
+            </div>
+            <div>
+              <h2 className="text-3xl font-black text-slate-900 tracking-tight">
+                Enterprise Intelligence
+              </h2>
+              <p className="text-slate-500 max-w-md mt-3 text-lg">
+                Sophisticated AI assistant for your professional workflow. Ask
+                anything from market analysis to complex coding solutions.
+              </p>
+            </div>
+          </div>
+        )}
+
+        {messages.map((msg, idx) => (
+          <div
+            key={idx}
+            className={`flex ${
+              msg.role === "user" ? "justify-end" : "justify-start"
+            } animate-in fade-in slide-in-from-bottom-2`}
+          >
+            <div
+              className={`max-w-[75%] rounded-3xl px-6 py-4 shadow-sm border ${
+                msg.role === "user"
+                  ? "bg-[#004ea8] text-white border-[#003d82] rounded-br-none shadow-blue-200 shadow-lg"
+                  : "bg-white text-slate-700 border-slate-100 rounded-bl-none shadow-md"
+              }`}
+            >
+              <p className="text-base leading-relaxed whitespace-pre-wrap font-medium">
+                {msg.text}
+              </p>
+              <div
+                className={`text-[10px] mt-3 font-bold uppercase tracking-widest ${
+                  msg.role === "user" ? "text-blue-100" : "text-[#004ea8]"
+                }`}
+              >
+                {msg.timestamp.toLocaleTimeString([], {
+                  hour: "2-digit",
+                  minute: "2-digit",
+                })}
+              </div>
+            </div>
+          </div>
+        ))}
+
+        {isTyping && (
+          <div className="flex justify-start">
+            <div className="bg-white border border-slate-100 rounded-3xl px-6 py-4 rounded-bl-none shadow-sm">
+              <div className="flex space-x-2">
+                <div className="w-2.5 h-2.5 bg-[#004ea8] rounded-full animate-bounce" />
+                <div className="w-2.5 h-2.5 bg-[#004ea8] rounded-full animate-bounce [animation-delay:-0.15s]" />
+                <div className="w-2.5 h-2.5 bg-[#004ea8] rounded-full animate-bounce [animation-delay:-0.3s]" />
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+
+      <div className="p-8 bg-white border-t border-slate-200">
+        <div className="max-w-5xl mx-auto relative">
+          <input
+            type="text"
+            value={input}
+            onChange={(e) => setInput(e.target.value)}
+            onKeyDown={(e) => e.key === "Enter" && handleSend()}
+            placeholder="Input your query here..."
+            className="w-full bg-slate-50 text-slate-900 rounded-2xl px-8 py-5 focus:outline-none focus:ring-4 focus:ring-blue-100 border border-slate-200 placeholder-slate-400 text-lg shadow-inner transition-all"
+          />
+          <button
+            disabled={isTyping}
+            onClick={handleSend}
+            className="absolute right-3 top-3 bottom-3 bg-[#004ea8] hover:bg-[#003d82] disabled:bg-slate-300 text-white rounded-xl px-8 transition-all font-bold shadow-lg flex items-center space-x-2 active:scale-95"
+          >
+            <span>Execute</span>
+            <svg
+              className="w-5 h-5"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                strokeLinecap="round"
+                strokeLinejoin="round"
+                strokeWidth={2}
+                d="M13 5l7 7-7 7M5 5l7 7-7 7"
+              />
+            </svg>
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ChatView;

+ 254 - 0
EsimLao/esim-vite/src/components/CompatibilityModal.tsx

@@ -0,0 +1,254 @@
+import React, { useState, useEffect } from "react";
+import { useAppDispatch, useAppSelector } from "../hooks/useRedux";
+import { closeCompatibilityModal } from "../features/popup/popupSlice";
+import { useMutation } from "@tanstack/react-query";
+import { startLoading, stopLoading } from "../features/loading/loadingSlice";
+import { contentApi } from "../apis/contentApi";
+import { DeviceMeta } from "../services/content/types";
+import { useTranslation } from "react-i18next";
+
+const CompatibilityModal = () => {
+  const popup = useAppSelector((state) => state.popup.compatibilityModal);
+  const dispatch = useAppDispatch();
+  const [brands, setBrands] = useState<DeviceMeta[] | []>([]);
+  const [deviceList, setDeviceList] = useState<Record<string, string[]>>([]);
+  const [selectedBrand, setSelectedBrand] = useState("");
+  const { t } = useTranslation();
+
+  useEffect(() => {
+    getDeviceMetaMutation.mutate();
+  }, []);
+
+  const getDeviceMetaMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await contentApi.LoadDeviceMetaData();
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopLoading());
+      console.log("Get device metadata response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Get device metadata successful");
+        setBrands(data.data.brands || []);
+        let deviceMap: Record<string, string[]> = {};
+        data.data.brands.forEach((element) => {
+          deviceMap[element.brand] = element.devices.map(
+            (device) => device.modelName,
+          );
+        });
+
+        setDeviceList(deviceMap);
+
+        setSelectedBrand(
+          data.data.brands.length > 0 ? data.data.brands[0].brand : "",
+        );
+      } else {
+        console.error("Get device metadata failed, no token received");
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopLoading());
+      console.error("Get device metadata error:", error.response.data);
+    },
+  });
+
+  if (popup.isOpen === false) {
+    return null;
+  }
+  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={() => {
+          dispatch(closeCompatibilityModal());
+        }}
+      ></div>
+
+      {/* Container is now a flex column with hidden overflow to manage scroll internally */}
+      <div className="relative bg-white rounded-[32px] w-full max-w-4xl max-h-[90vh] shadow-2xl flex flex-col overflow-hidden">
+        {/* Fixed Close Button (Absolute) */}
+        <button
+          onClick={() => {
+            dispatch(closeCompatibilityModal());
+          }}
+          className="absolute top-4 right-4 p-2 text-slate-400 hover:text-[#EE0434] transition-colors bg-white/80 backdrop-blur-sm rounded-full shadow-sm z-20"
+        >
+          <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>
+
+        {/* Fixed Header */}
+        <div className="shrink-0 p-8 md:p-10 lg:p-10 pb-2 text-center bg-white z-10 relative">
+          <h2 className="text-xl md:text-3xl font-black text-slate-900 mb-2 tracking-tight">
+            {t("doesEsimWorkWithMyDevice")} <br />
+            {/* <span className="text-[#EE0434]">support eSIM ?</span> */}
+          </h2>
+        </div>
+
+        {/* Scrollable Content Body */}
+        <div className="flex-1 overflow-y-auto p-3 md:p-8 lg:p-10 pt-1 custom-scrollbar">
+          <div className="text-center mb-8">
+            <h3 className="text-xm md:text-base lg:text-xl font-black text-[#EE0434] mb-6">
+              {t("selectTopicInstructions")}
+            </h3>
+
+            <div className="text-left bg-slate-50 p-3 rounded-2xl border border-slate-100 text-sm md:text-base text-slate-600 space-y-4 mb-8">
+              <p className="font-bold">{t("importantNote")}</p>
+              <ul className="list-disc pl-5 space-y-1 marker:text-[#EE0434]">
+                <li>{t("someDualPhysicalSimModelsDoNotSupportEsim")}</li>
+                <li>{t("carrierLockedDevicesMayNotSupportEsim")}</li>
+                <li>
+                  {t("someModelsPurchasedInCertainCountriesMayNotSupportEsim")}
+                </li>
+              </ul>
+              <p>{t("toMakeSureYourDeviceSupportsEsim")}</p>
+            </div>
+          </div>
+
+          {/* Brand Tabs */}
+          <div className="flex justify-center space-x-8 mb-10 overflow-x-auto py-2">
+            {brands.map((brand) => (
+              <button
+                key={brand.brand}
+                onClick={() => setSelectedBrand(brand.brand)}
+                className={`flex flex-col items-center space-y-3 group min-w-[80px] focus:outline-none`}
+              >
+                <div
+                  className={`w-16 h-16 flex items-center justify-center transition-colors ${
+                    selectedBrand === brand.brand
+                      ? "text-slate-900 scale-110"
+                      : "text-slate-300 group-hover:text-slate-500"
+                  }`}
+                >
+                  {brand.brand == "Apple" ? (
+                    <svg
+                      className="w-8 h-8"
+                      viewBox="0 0 384 512"
+                      fill="currentColor"
+                    >
+                      <path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 52.3-11.4 69.5-34.3z" />
+                    </svg>
+                  ) : brand.brand == "Samsung" ? (
+                    <svg
+                      className="w-12 h-8"
+                      viewBox="0 0 24 24"
+                      fill="currentColor"
+                    >
+                      <path d="M17.8 4.8C14.3 3.6 9.7 3.6 6.2 4.8 1.9 6.3 0 10.2 0 14.9s1.9 8.6 6.2 10.1c3.5 1.2 8.1 1.2 11.6 0 4.3-1.5 6.2-5.4 6.2-10.1s-1.9-8.6-6.2-10.1zM8 17.9c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4zm0-5.7c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4zm8 5.7c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4zm0-5.7c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4z" />
+                    </svg>
+                  ) : brand.brand == "Google" ? (
+                    <svg
+                      className="w-8 h-8"
+                      viewBox="0 0 24 24"
+                      fill="currentColor"
+                    >
+                      <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
+                    </svg>
+                  ) : (
+                    <svg
+                      className="w-6 h-8"
+                      viewBox="0 0 24 24"
+                      fill="currentColor"
+                    >
+                      <path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z" />
+                    </svg>
+                  )}
+                </div>
+                <span
+                  className={`font-bold transition-colors text-lg md:text-lg lg:text-xl ${
+                    selectedBrand === brand.brand
+                      ? "text-[#EE0434]"
+                      : "text-slate-300 group-hover:text-slate-500"
+                  }`}
+                >
+                  {brand.brand}
+                </span>
+                {selectedBrand === brand.brand && (
+                  <div className="w-12 h-1 bg-[#EE0434] rounded-full"></div>
+                )}
+              </button>
+            ))}
+          </div>
+
+          {/* Device List (Scrolls with the body) */}
+          <div className="bg-white rounded-t-3xl border-t border-slate-100">
+            <div className="divide-y divide-slate-50">
+              {deviceList[selectedBrand].map((device, idx) => (
+                <div
+                  key={idx}
+                  className="py-4 px-4 text-center hover:bg-slate-50 transition-colors text-slate-700 font-bold text-x,"
+                >
+                  {device}
+                </div>
+              ))}
+            </div>
+          </div>
+
+          {/* Divider */}
+          <div className="my-10 border-t border-dashed border-slate-300"></div>
+
+          {/* Apple Specific Note */}
+          <div className="text-left text-slate-600 space-y-4 mb-12">
+            <p className="font-bold text-[#EE0434]">
+              {t("noteTheFollowingAppleDevicesDoNotSupportEsim")}
+            </p>
+            <ul className="list-disc pl-5 space-y-2 text-sm md:text-base">
+              <li>{t("someDualPhysicalSimModelsDoNotSupportEsim")}</li>
+              <li>
+                {t(
+                  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim",
+                )}
+              </li>
+            </ul>
+          </div>
+
+          {/* Divider */}
+          <div className="my-10 border-t border-dashed border-slate-300"></div>
+
+          {/* Method 2 */}
+          <div className="text-center mb-12">
+            <h3 className="text-xl md:text-2xl font-black text-[#EE0434] mb-4">
+              {t("method2CheckDirectlyOnYourDevice")}
+            </h3>
+            <p className="text-slate-600 text-xs md:text-lg lg:text-xl">
+              {t(
+                "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing",
+              )}
+              {/* <button className="text-[#EE0434] font-bold hover:underline">
+                {" "}
+                {t("theStepsBelow")}
+              </button> */}
+            </p>
+          </div>
+
+          {/* Not Support */}
+          <div className="text-center mb-8">
+            <h3 className="text-xl md:text-2xl font-black text-[#EE0434] mb-4">
+              {t("yourDeviceDoesNotSupportEsim")}
+            </h3>
+            <p className="text-slate-600 text-xs md:text-lg lg:text-xl">
+              {t("tryFindingAPhysicalSimThatIsCompatibleWithYourDevice")}{" "}
+              <button className="text-[#EE0434] font-bold hover:underline">
+                {t("here")}
+              </button>
+            </p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default CompatibilityModal;

+ 253 - 0
EsimLao/esim-vite/src/components/Footer.tsx

@@ -0,0 +1,253 @@
+import React, { useEffect, useState } from "react";
+import logo from "../assets/img/getgo.svg";
+import { useTranslation } from "react-i18next";
+import { getWithExpiry } from "../logic/loigicUtils";
+import { Area } from "../services/product/type";
+import { useNavigate } from "react-router-dom";
+import { DataCacheKey, staleTime } from "../global/constants";
+import { startLoading, stopLoading } from "../features/loading/loadingSlice";
+import { useAppDispatch } from "../hooks/useRedux";
+import { useQuery } from "@tanstack/react-query";
+import { productApi } from "../apis/productApi";
+const Footer: React.FC = () => {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const dispatch = useAppDispatch();
+
+  const { data: loadArea = [] } = useQuery<Area[]>({
+    queryKey: [DataCacheKey.AREAS_FOOTER],
+    queryFn: async (): Promise<Area[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await productApi.loadArea({
+          isCountry: "1",
+          isPopular: "1",
+        });
+        // save to redux store
+        console.log("Get area response data:", res);
+        return res.data as Area[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
+    },
+    staleTime: staleTime,
+  });
+
+  const popularAreas = React.useMemo(() => loadArea.slice(0, 3), [loadArea]);
+
+  return (
+    <footer className="bg-white border-t border-slate-100 pt-16 pb-8">
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+        {/* Info Columns */}
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12 mb-16">
+          <div className="space-y-6">
+            <div className="flex items-center space-x-2">
+              {/* <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> */}
+              <img
+                src={logo}
+                alt="SkySimHub Logo"
+                className="text-[#EE0434] mr-0"
+                style={{ width: "150px" }}
+              />
+              {/* <span className="text-xl font-black text-[#EE0434]">etgo</span> */}
+            </div>
+            <p className="text-sm font-bold text-[#EE0434] leading-tight">
+              Công ty TNHH Phát triển toàn cầu VIETTECH
+            </p>
+            <p className="text-sm font-medium text-slate-600">
+              Mã số thuế: 0901210362
+            </p>
+            <p className="text-sm font-medium text-slate-600">
+              Ngày cấp: 09/01/2026
+            </p>
+            <p className="text-sm font-medium text-slate-600">
+              Nơi cấp: Phòng Đăng Ký Kinh Doanh Tỉnh Hưng Yên - Sở Tài Chính
+              Tỉnh Hưng Yên
+            </p>
+            <div className="space-y-3 text-sm text-slate-500">
+              <p className="flex items-start">
+                <svg
+                  className="w-4 h-4 mr-2 mt-0.5 text-[#EE0434]"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2}
+                    d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
+                  />
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2}
+                    d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
+                  />
+                </svg>{" "}
+                218 Đảo Dừa 1, Khu đô thị Vinhomes Oceanpark 2, Xã Nghĩa Trụ,
+                Tỉnh Hưng Yên, Việt Nam
+              </p>
+              <p className="flex items-center">
+                <svg
+                  className="w-4 h-4 mr-2 text-[#EE0434]"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2}
+                    d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
+                  />
+                </svg>{" "}
+                klinhnguyen@viettech.asia
+              </p>
+              <p className="flex items-center">
+                <svg
+                  className="w-4 h-4 mr-2 text-[#EE0434]"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2}
+                    d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
+                  />
+                </svg>{" "}
+                Zalo/Whatsapp: +84336548007
+              </p>
+            </div>
+          </div>
+          <div>
+            <h3 className="text-base font-bold text-slate-900 mb-6">
+              SkySimHub
+            </h3>
+            <ul className="space-y-4 text-sm text-slate-500">
+              <li>
+                <a href="#" className="hover:text-[#EE0434] transition-colors">
+                  {t("aboutUs")}
+                </a>
+              </li>
+              <li>
+                <a
+                  href="#"
+                  className="hover:text-[#EE0434] transition-colors"
+                  onClick={() => navigate("/terms")}
+                >
+                  {t("termsService")}
+                </a>
+              </li>
+              {/* <li>
+                <a href="#" className="hover:text-[#EE0434] transition-colors">
+                  {t("privacyPolicy")}
+                </a>
+              </li> */}
+            </ul>
+          </div>
+          <div>
+            <h3 className="text-base font-bold text-slate-900 mb-6">
+              Travel {t("sim")}
+            </h3>
+            <ul className="space-y-4 text-sm text-slate-500">
+              {popularAreas.map((area) => (
+                <li>
+                  <a
+                    href="#"
+                    className="hover:text-[#EE0434] transition-colors"
+                    onClick={() =>
+                      navigate(`/product-detail/${area.id}`, {
+                        state: {
+                          ...area,
+                        },
+                      })
+                    }
+                  >
+                    {area.areaName1}
+                  </a>
+                </li>
+              ))}
+              {/* <li>
+                <a
+                  href="#"
+                  className="hover:text-[#EE0434] transition-colors"
+                  onClick={() => navigate("/buy-sim")}
+                >
+                  {t("chinaSim")}
+                </a>
+              </li>
+              <li>
+                <a
+                  href="#"
+                  className="hover:text-[#EE0434] transition-colors"
+                  onClick={() => navigate("/buy-sim")}
+                >
+                  {t("thailandSim")}
+                </a>
+              </li>
+              <li>
+                <a
+                  href="#"
+                  className="hover:text-[#EE0434] transition-colors"
+                  onClick={() => navigate("/buy-sim")}
+                >
+                  {t("japanSim")}
+                </a>
+              </li> */}
+            </ul>
+          </div>
+          {/* <div>
+            <h3 className="text-base font-bold text-slate-900 mb-6">Guide</h3>
+            <ul className="space-y-4 text-sm text-slate-500">
+              <li>
+                <a href="#" className="hover:text-[#EE0434] transition-colors">
+                  {t("whatEsim")}
+                </a>
+              </li>
+              <li>
+                <a href="#" className="hover:text-[#EE0434] transition-colors">
+                  {t("supportSupport")}
+                </a>
+              </li>
+            </ul>
+          </div> */}
+        </div>
+
+        {/* Bottom */}
+        <div className="pt-8 border-t border-slate-100 flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
+          <p className="text-sm text-slate-400">
+            © 2025 SkySimHub All rights reserved
+          </p>
+          <div className="bg-[#EE0434] px-4 py-2 rounded-lg flex items-center space-x-2">
+            <div className="w-5 h-5 bg-white rounded-full flex items-center justify-center text-[#EE0434]">
+              <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
+                <path
+                  fillRule="evenodd"
+                  d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
+                  clipRule="evenodd"
+                />
+              </svg>
+            </div>
+            <span className="text-[10px] text-white font-bold uppercase tracking-widest">
+              Official Store Verified
+            </span>
+          </div>
+        </div>
+      </div>
+    </footer>
+  );
+};
+
+export default Footer;

+ 1054 - 0
EsimLao/esim-vite/src/components/Header.tsx

@@ -0,0 +1,1054 @@
+import React, { useState, useEffect, useRef } from "react";
+import { useNavigate, useLocation, Link } from "react-router-dom";
+// import logo from "../assets/img/getgo.svg";
+import { useAppDispatch, useAppSelector } from "../hooks/useRedux";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import {
+  startLoading,
+  startSmallLoading,
+  stopLoading,
+  stopSmallLoading,
+} from "../features/loading/loadingSlice";
+import { productApi } from "../apis/productApi";
+import { Area } from "../services/product/type";
+import { accountLogout } from "../features/account/accuntSlice";
+import { useSelector } from "react-redux";
+import { setAreas } from "../features/areas/areasSlice";
+import i18n from "../i18n";
+import { useTranslation } from "react-i18next";
+import {
+  formatCurrency,
+  getWithExpiry,
+  setWithExpiry,
+} from "../logic/loigicUtils";
+import { Faq } from "../services/content/types";
+import { DataCacheKey, staleTime } from "../global/constants";
+import { contentApi } from "../apis/contentApi";
+import logo from "../assets/img/logo-skysim.png";
+
+const Header: React.FC = () => {
+  const navigate = useNavigate();
+  // const areas = useSelector((state: any) => state.areas.areas);
+  const areas = getWithExpiry<Area[] | []>("areas");
+  const { t } = useTranslation();
+  const location = useLocation();
+  const [isMenuOpen, setIsMenuOpen] = useState(false);
+  const [isBuySimExpanded, setIsBuySimExpanded] = useState(false);
+  const [isGuideExpanded, setIsGuideExpanded] = useState(false);
+  const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
+  const [isBuySimMegaVisible, setIsBuySimMegaVisible] = useState(false);
+  const [isGuideMegaVisible, setIsGuideMegaVisible] = useState(false);
+  const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
+  const lang = getWithExpiry("selectedLanguage") || "en";
+
+  const [selectedLang, setSelectedLang] = useState<"en" | "vi">(lang);
+  const [activeDesktopTab, setActiveDesktopTab] = useState<
+    "popular" | "region"
+  >("popular");
+  const [isScrolled, setIsScrolled] = useState(false);
+  const [products, setProducts] = useState<Area[]>([]);
+  const dispatch = useAppDispatch();
+  const langMenuRef = useRef<HTMLDivElement>(null);
+  const userMenuRef = useRef<HTMLDivElement>(null);
+  const account = getWithExpiry("accountInfo");
+  const partner = getWithExpiry("selectedPartner") as string | null;
+
+  const guideItems = [
+    { label: "Giới thiệu cơ bản về eSim", path: "/support" },
+    { label: "Hướng dẫn cài đặt eSim trên các thiết bị", path: "/support" },
+    { label: "Hướng dẫn bật chuyển vùng dữ liệu (Roaming)", path: "/support" },
+    { label: "Cách kiểm tra dung lượng đã dùng", path: "/support" },
+    { label: "Cách kiểm tra eSim đang sử dụng dữ liệu", path: "/support" },
+    { label: "Cách ước tính dung lượng", path: "/support" },
+    { label: "Các sự cố eSim phổ biến", path: "/support" },
+  ];
+  const dropdownRef = useRef<HTMLDivElement>(null);
+  const [areasList, setAreasList] = useState<Area[]>([]);
+  const [searchQuery, setSearchQuery] = useState("");
+  const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
+
+  const languages =
+    partner === "laotravel"
+      ? lang === "la"
+        ? [
+            { code: "la", label: t("lao"), flag: "la" },
+            { code: "vi", label: t("vietnamese"), flag: "vn" },
+          ]
+        : [
+            { code: "vi", label: t("vietnamese"), flag: "vn" },
+            { code: "la", label: t("lao"), flag: "la" },
+          ]
+      : [
+          // { code: "en", label: t("english"), flag: "us" },
+          { code: "vi", label: t("vietnamese"), flag: "vn" },
+        ];
+
+  // load product by country/region or popularity
+  const getProductMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await productApi.loadArea(
+        activeDesktopTab === "popular"
+          ? { isCountry: "1", isPopular: "1" }
+          : { isCountry: "0", isPopular: "1" },
+      );
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopLoading());
+      if (data && data.errorCode === "0") {
+        setProducts(data.data);
+      } else {
+        console.error("Get area failed, no token received");
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopLoading());
+      console.error("Get area error:", error.response.data);
+    },
+  });
+
+  const handleAreaClick = (c: { id: number }) => {
+    navigate(`/product-detail/${c.id}`, {
+      state: {
+        ...c,
+      },
+    });
+    setIsBuySimMegaVisible(false);
+    setIsMenuOpen(false);
+  };
+
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (
+        langMenuRef.current &&
+        !langMenuRef.current.contains(event.target as Node)
+      ) {
+        setIsLangMenuOpen(false);
+      }
+    };
+    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);
+  }, []);
+
+  const langNow = getWithExpiry("selectedLanguage") || "en";
+  const currentLangObj =
+    languages.find((l) => l.code === (selectedLang || langNow)) || languages[0];
+
+  const isActive = (path: string) => location.pathname === path;
+
+  useEffect(() => {
+    getProductMutation.mutate();
+  }, [activeDesktopTab]);
+
+  useEffect(() => {
+    if (!areas || areas.length === 0) getAreaMutation.mutate();
+    else {
+      setAreasList(areas);
+    }
+  }, []);
+
+  useEffect(() => {
+    const handleScroll = () => {
+      setIsScrolled(window.scrollY > 300);
+    };
+    window.addEventListener("scroll", handleScroll);
+    return () => window.removeEventListener("scroll", handleScroll);
+  }, []);
+
+  const getAreaMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await productApi.loadArea({
+        isCountry: "-1",
+        isPopular: "-1",
+      });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopLoading());
+      console.log("Get area response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Get area successful");
+        setWithExpiry("areas", data.data);
+        setAreasList(data.data as Area[]);
+      } else {
+        console.error("Get area failed, no token received");
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopLoading());
+      console.error("Get area error:", error.response.data);
+    },
+  });
+
+  const handleSelect = (area: Area) => {
+    console.log("Selected area:", area);
+    setIsSearchDropdownOpen(false);
+    navigate(`/product-detail/${area.id}`, {
+      state: {
+        ...area,
+      },
+    });
+  };
+
+  const handleSearch = (query: string) => {
+    setSearchQuery(query);
+    if (query.trim() === "") {
+      setAreasList(areas);
+    } else {
+      const filtered = areas.filter((area: Area) =>
+        area.searchArea?.toLowerCase().includes(query.toLowerCase()),
+      );
+      setAreasList(filtered);
+    }
+  };
+
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (
+        dropdownRef.current &&
+        !dropdownRef.current.contains(event.target as Node)
+      ) {
+        setIsSearchDropdownOpen(false);
+      }
+    };
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => {
+      document.removeEventListener("mousedown", handleClickOutside);
+    };
+  }, []);
+
+  // const { data: loadFaqCateData = [] } = useQuery<Faq[]>({
+  //   queryKey: [DataCacheKey.FAQS_HEADER],
+  //   queryFn: async (): Promise<Faq[]> => {
+  //     try {
+  //       dispatch(startLoading({}));
+  //       const res = await contentApi.LoadFaq({
+  //         pageNumber: 0,
+  //         pageSize: 10,
+  //         isFeatured: false,
+  //       });
+  //       const data = res.data.faqs as Faq[];
+  //       setWithExpiry(DataCacheKey.FAQS_HEADER, data);
+  //       return data;
+  //     } catch (error) {
+  //       console.error(error);
+  //       return []; // 🔴 bắt buộc
+  //     } finally {
+  //       dispatch(stopLoading());
+  //     }
+  //   },
+  //   staleTime: staleTime,
+  // });
+
+  // const guideItems = React.useMemo(() => loadFaqCateData, [loadFaqCateData]);
+
+  return (
+    <>
+      <header className="sticky top-0 z-[60] w-full bg-white border-b border-slate-100 shadow-sm transition-all duration-300">
+        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+          <div className="flex justify-between items-center h-20">
+            {/* 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">
+                  <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
+                </svg> */}
+                <img src={logo} alt="SkySimHub 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> */}
+              </div>
+            </Link>
+
+            {/* Desktop Search on Scroll */}
+            <div
+              ref={dropdownRef}
+              className={`hidden lg:flex items-center transition-all duration-500 ${
+                isScrolled
+                  ? "flex-1 max-w-md mx-8 opacity-100 overflow-visible"
+                  : "max-w-0 opacity-0 pointer-events-none overflow-hidden"
+              }`}
+            >
+              <div className="relative w-full">
+                <input
+                  type="text"
+                  value={searchQuery}
+                  onChange={(e) => {
+                    handleSearch(e.target.value);
+                  }}
+                  onFocus={() => setIsSearchDropdownOpen(true)}
+                  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
+                  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>
+
+                {/* Dropdown Menu */}
+                {isSearchDropdownOpen && (
+                  <div className="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.1)] border border-slate-100 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
+                    <div className="max-h-[300px] overflow-y-auto custom-scrollbar p-2">
+                      <h3 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2 px-3 pt-2">
+                        {t("mostPopular")}
+                      </h3>
+                      <div className="space-y-0.5">
+                        {areasList.length > 0 ? (
+                          areasList.map((p) => (
+                            <button
+                              key={p.id}
+                              onClick={() => handleSelect(p)}
+                              className="w-full flex items-center justify-between p-2 hover:bg-slate-50 rounded-xl transition-all group"
+                            >
+                              <div className="flex items-center space-x-3">
+                                <div className="w-8 h-8 rounded-full border border-slate-100 overflow-hidden shadow-sm shrink-0">
+                                  <img
+                                    src={`${p.iconUrl}`}
+                                    alt={p.areaName1}
+                                    className="w-full h-full object-cover scale-150"
+                                  />
+                                </div>
+                                <span className="font-bold text-slate-700 text-sm group-hover:text-[#EE0434] transition-colors">
+                                  {p.areaName1}
+                                </span>
+                              </div>
+                              <div className="text-right flex items-center space-x-1.5">
+                                <span className="text-xs text-slate-400 font-medium">
+                                  {t("from")}:
+                                </span>
+                                <span className="text-sm font-black text-[#EE0434]">
+                                  {formatCurrency(p.minSellPrice, p.curency)}
+                                </span>
+                              </div>
+                            </button>
+                          ))
+                        ) : (
+                          <div className="text-center py-4 text-slate-400 text-sm font-medium">
+                            {t("noMatchesFound")}
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </div>
+                )}
+              </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]"
+                }`}
+              >
+                {t("home")}
+              </Link>
+
+              <div
+                className="relative h-full flex items-center"
+                onMouseEnter={() => setIsBuySimMegaVisible(true)}
+                onMouseLeave={() => setIsBuySimMegaVisible(false)}
+              >
+                <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]"
+                  }`}
+                >
+                  {t("buySim")}{" "}
+                  <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">
+                        {t("buySim")}
+                      </h3>
+                      <button
+                        onClick={() => {
+                          navigate("/buy-sim");
+                          setIsBuySimMegaVisible(false);
+                        }}
+                        className="text-[#EE0434] font-bold text-xl flex items-center group mb-8"
+                      >
+                        {t("viewAll")}{" "}
+                        <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"
+                          }`}
+                        >
+                          {t("mostPopular")}
+                        </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"
+                          }`}
+                        >
+                          {t("region")}
+                        </button>
+                      </div>
+                      <div className="grid grid-cols-4 gap-y-8 gap-x-4">
+                        {products.map((p) => (
+                          <div
+                            key={p.id}
+                            onClick={() => handleAreaClick(p)}
+                            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={`${p.iconUrl}`}
+                                alt={p.areaName1}
+                                className="w-full h-full object-cover"
+                              />
+                            </div>
+                            <span className="text-[16px] font-bold text-slate-700 group-hover:text-[#EE0434] transition-colors">
+                              {p.areaName1}
+                            </span>
+                          </div>
+                        ))}
+                      </div>
+                    </div>
+                  </div>
+                )}
+              </div>
+
+              {partner !== "laotravel" && (
+                <div
+                  className="relative h-full flex items-center"
+                  onMouseEnter={() => setIsGuideMegaVisible(true)}
+                  onMouseLeave={() => setIsGuideMegaVisible(false)}
+                >
+                  <Link
+                    to="/support"
+                    className={`flex items-center text-[17px] font-bold transition-colors ${
+                      isActive("/support")
+                        ? "text-[#EE0434]"
+                        : "text-slate-700 hover:text-[#EE0434]"
+                    }`}
+                  >
+                    {t("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}
+                            onClick={() => {
+                              navigate("/support");
+                              setIsGuideMegaVisible(false);
+                            }}
+                            className={`w-full text-left px-6 py-4 rounded-2xl text-base font-medium transition-all text-slate-600 hover:bg-slate-50 hover:text-[#EE0434]`}
+                          >
+                            {item.label}
+                          </button>
+                        ))}
+                      </div>
+                    </div>
+                  )}
+                </div>
+              )}
+
+              <Link
+                to="/news"
+                className={`text-[17px] font-bold transition-colors ${
+                  isActive("/news")
+                    ? "text-[#EE0434]"
+                    : "text-slate-700 hover:text-[#EE0434]"
+                }`}
+              >
+                {t("news")}
+              </Link>
+
+              <Link
+                to="/contact"
+                className={`text-[17px] font-bold transition-colors ${
+                  isActive("/contact")
+                    ? "text-[#EE0434]"
+                    : "text-slate-700 hover:text-[#EE0434]"
+                }`}
+              >
+                {t("contact")}
+              </Link>
+            </nav>
+
+            {/* Icons */}
+            <div className="flex items-center space-x-5">
+              {/* Account Dropdown */}
+              <div className="relative hidden lg:block" ref={userMenuRef}>
+                <button
+                  onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
+                  className="p-2 text-slate-700 hover:text-[#EE0434] transition-colors rounded-full hover:bg-slate-50"
+                  title="Account"
+                >
+                  <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>
+                </button>
+
+                {isUserMenuOpen && (
+                  <div className="absolute top-full right-0 mt-3 w-56 bg-white rounded-[24px] shadow-[0_20px_40px_rgba(0,0,0,0.1)] border border-slate-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200 z-50">
+                    {account !== null && (
+                      <div className="flex flex-col py-2">
+                        <button
+                          onClick={() => {
+                            navigate("/order-history");
+                            setIsUserMenuOpen(false);
+                          }}
+                          className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
+                        >
+                          <svg
+                            className="w-5 h-5 text-slate-400"
+                            fill="none"
+                            stroke="currentColor"
+                            viewBox="0 0 24 24"
+                          >
+                            <path
+                              strokeLinecap="round"
+                              strokeLinejoin="round"
+                              strokeWidth={2}
+                              d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
+                            />
+                          </svg>
+                          <span className="font-bold text-sm">
+                            {t("orders")}
+                          </span>
+                        </button>
+                        <div className="h-px bg-slate-50 mx-4 my-1"></div>
+
+                        <button
+                          onClick={() => {
+                            setIsUserMenuOpen(false);
+                            dispatch(accountLogout());
+                            navigate("/login");
+                          }}
+                          className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
+                        >
+                          <svg
+                            className="w-5 h-5 text-slate-400"
+                            fill="none"
+                            stroke="currentColor"
+                            viewBox="0 0 24 24"
+                          >
+                            <path
+                              strokeLinecap="round"
+                              strokeLinejoin="round"
+                              strokeWidth={2}
+                              d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
+                            />
+                          </svg>
+                          <span className="font-bold text-sm">
+                            {t("logout")}
+                          </span>
+                        </button>
+                      </div>
+                    )}
+                    {account === null && (
+                      <div className="flex flex-col py-2">
+                        <button
+                          onClick={() => {
+                            setIsUserMenuOpen(false);
+                            dispatch(accountLogout());
+                            navigate("/login");
+                          }}
+                          className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
+                        >
+                          <svg
+                            className="w-5 h-5 text-slate-400"
+                            fill="none"
+                            stroke="currentColor"
+                            viewBox="0 0 24 24"
+                          >
+                            <path
+                              strokeLinecap="round"
+                              strokeLinejoin="round"
+                              strokeWidth={2}
+                              d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
+                            />
+                          </svg>
+                          <span className="font-bold text-sm">
+                            {t("login")}
+                          </span>
+                        </button>
+                      </div>
+                    )}
+                  </div>
+                )}
+              </div>
+              {/* <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>
+              </button> */}
+
+              <div className="relative" ref={langMenuRef}>
+                <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}
+                      className="w-full h-full object-cover"
+                    />
+                  </div>
+                </button>
+
+                {isLangMenuOpen && (
+                  <div className="absolute top-full right-0 mt-3 w-48 bg-white rounded-[24px] shadow-[0_20px_40px_rgba(0,0,0,0.1)] border border-slate-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
+                    <div className="flex flex-col">
+                      {languages.map((lang) => (
+                        <button
+                          key={lang.code}
+                          onClick={() => {
+                            setSelectedLang(lang.code as "en" | "vi" | "la");
+                            setIsLangMenuOpen(false);
+                            i18n.changeLanguage(lang.code);
+                            setWithExpiry("selectedLanguage", lang.code);
+                          }}
+                          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"
+                          }`}
+                        >
+                          <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}
+                              className="w-full h-full object-cover"
+                            />
+                          </div>
+                          <span
+                            className={`text-[15px] font-bold ${
+                              selectedLang === lang.code
+                                ? "text-slate-900"
+                                : "text-slate-500"
+                            }`}
+                          >
+                            {lang.label}
+                          </span>
+                        </button>
+                      ))}
+                    </div>
+                  </div>
+                )}
+              </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>
+            </div>
+          </div>
+        </div>
+      </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"
+          }`}
+        >
+          {/* 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"
+              >
+                <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">
+                <span className="text-[#EE0434]">Get</span>
+                <span className="text-[#333]">Go</span>
+              </span> */}
+              <img src={logo} alt="Sky Sim Hub Logo" className="h-12" />
+            </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>
+          </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="/"
+                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"
+                }`}
+              >
+                {t("home")}
+              </Link>
+
+              <div className="w-full">
+                <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"
+                  }`}
+                >
+                  <span>{t("buySim")}</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>
+                </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="grid grid-cols-2 gap-3 px-2">
+                    <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"
+                    >
+                      {t("viewAllDestinations")} →
+                    </button>
+                    {products.map((c) => (
+                      <button
+                        key={c.areaName1}
+                        onClick={() => handleAreaClick(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={`${c.iconUrl}`}
+                          alt={c.areaName1}
+                          className="w-10 h-10 rounded-full object-cover border-2 border-slate-50"
+                        />
+                        <span className="text-sm font-bold text-slate-700">
+                          {c.areaName1}
+                        </span>
+                      </button>
+                    ))}
+                  </div>
+                </div>
+              </div>
+
+              {partner !== "laotravel" && (
+                <div className="w-full">
+                  <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"
+                    }`}
+                  >
+                    <span>{t("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>
+                  </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="flex flex-col space-y-2 px-2">
+                      {guideItems.map((item) => (
+                        <button
+                          key={item.label}
+                          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}
+                        </button>
+                      ))}
+                    </div>
+                  </div>
+                </div>
+              )}
+
+              <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"
+                }`}
+              >
+                {t("news")}
+              </Link>
+
+              <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"
+                }`}
+              >
+                {t("contact")}
+              </Link>
+
+              {account !== null && (
+                <button
+                  onClick={() => {
+                    navigate("/order-history");
+                    setIsMenuOpen(false);
+                  }}
+                  className={`w-full text-center py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
+                    isActive("/order-history")
+                      ? "bg-red-50 text-[#EE0434]"
+                      : "text-slate-800 hover:bg-slate-50"
+                  }`}
+                >
+                  {t("transactionHistory")}
+                </button>
+              )}
+
+              <div className="w-full pt-4">
+                <p className="text-center text-slate-400 font-bold text-xs uppercase tracking-widest mb-4">
+                  {t("selectLanguage")}
+                </p>
+                <div className="flex justify-center space-x-4">
+                  {languages.map((lang) => (
+                    <button
+                      key={lang.code}
+                      onClick={() => {
+                        setSelectedLang(lang.code as "en" | "vi" | "la");
+                        setIsLangMenuOpen(false);
+                        i18n.changeLanguage(lang.code);
+                        setWithExpiry("selectedLanguage", lang.code);
+                      }}
+                      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">
+            {account === null && (
+              <Link
+                to="/login"
+                onClick={() => {
+                  setIsMenuOpen(false);
+                  dispatch(accountLogout());
+                }}
+                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"
+              >
+                {t("login")} / {t("register")}
+              </Link>
+            )}
+            {account !== null && (
+              <Link
+                to="/login"
+                onClick={() => {
+                  setIsMenuOpen(false);
+                  dispatch(accountLogout());
+                }}
+                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"
+              >
+                {t("logout")}
+              </Link>
+            )}
+          </div>
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default Header;

+ 141 - 0
EsimLao/esim-vite/src/components/ImageView.tsx

@@ -0,0 +1,141 @@
+import React, { useState } from "react";
+import { generateImage } from "../services/gemini";
+import { GeneratedImage } from "../services/types";
+
+const ImageView: React.FC = () => {
+  const [prompt, setPrompt] = useState("");
+  const [loading, setLoading] = useState(false);
+  const [history, setHistory] = useState<GeneratedImage[]>([]);
+  const [aspectRatio, setAspectRatio] = useState<"1:1" | "16:9" | "9:16">(
+    "1:1"
+  );
+
+  const handleGenerate = async () => {
+    if (!prompt.trim() || loading) return;
+    setLoading(true);
+    try {
+      const url = await generateImage(prompt, aspectRatio);
+      setHistory((prev) => [{ url, prompt, timestamp: new Date() }, ...prev]);
+    } catch (error) {
+      console.error(error);
+      alert("Asset generation failed. Please refine your parameters.");
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="flex flex-col h-full bg-slate-50">
+      <div className="p-10 bg-white border-b border-slate-200 shadow-sm z-10">
+        <div className="max-w-7xl mx-auto flex flex-col space-y-6">
+          <div>
+            <h2 className="text-3xl font-black text-slate-900 tracking-tight">
+              Image Forge
+            </h2>
+            <p className="text-slate-500 font-medium">
+              Generate high-fidelity visual assets for your brand and projects.
+            </p>
+          </div>
+          <div className="flex flex-col lg:flex-row gap-4">
+            <div className="flex-1 relative group">
+              <input
+                type="text"
+                value={prompt}
+                onChange={(e) => setPrompt(e.target.value)}
+                placeholder="High-resolution architectural render of a modern workspace..."
+                className="w-full bg-slate-50 text-slate-800 rounded-2xl px-6 py-4 focus:outline-none focus:ring-4 focus:ring-blue-500/10 border border-slate-200 text-lg transition-all"
+              />
+            </div>
+            <div className="flex gap-4">
+              <select
+                value={aspectRatio}
+                onChange={(e: any) => setAspectRatio(e.target.value)}
+                className="bg-white text-slate-700 border border-slate-200 rounded-2xl px-6 py-4 outline-none focus:ring-2 focus:ring-[#004ea8] font-semibold text-sm shadow-sm cursor-pointer hover:bg-slate-50 transition-colors"
+              >
+                <option value="1:1">Standard (1:1)</option>
+                <option value="16:9">Widescreen (16:9)</option>
+                <option value="9:16">Mobile (9:16)</option>
+              </select>
+              <button
+                onClick={handleGenerate}
+                disabled={loading}
+                className="bg-[#004ea8] hover:bg-[#003d82] disabled:opacity-50 text-white rounded-2xl px-10 py-4 font-bold transition-all shadow-xl active:scale-95 whitespace-nowrap"
+              >
+                {loading ? "Synthesizing..." : "Generate Assets"}
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div className="flex-1 overflow-y-auto p-10">
+        <div className="max-w-7xl mx-auto">
+          {history.length === 0 && !loading && (
+            <div className="h-[50vh] flex flex-col items-center justify-center opacity-40">
+              <div className="p-10 bg-white rounded-[40px] shadow-2xl border border-slate-100 mb-8">
+                <svg
+                  className="w-24 h-24 text-slate-200"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={1}
+                    d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
+                  />
+                </svg>
+              </div>
+              <p className="text-2xl font-bold text-slate-400">
+                Project Gallery Empty
+              </p>
+              <p className="text-slate-300 mt-2">
+                Initialize generation to view results.
+              </p>
+            </div>
+          )}
+
+          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
+            {loading && (
+              <div className="aspect-square bg-white border border-slate-200 rounded-[32px] flex flex-col items-center justify-center shadow-xl animate-pulse">
+                <div className="w-16 h-16 border-4 border-[#004ea8] border-t-transparent rounded-full animate-spin"></div>
+                <p className="mt-6 text-slate-500 font-bold uppercase tracking-widest text-xs">
+                  Computing Vision...
+                </p>
+              </div>
+            )}
+            {history.map((img, i) => (
+              <div
+                key={i}
+                className="group relative bg-white border border-slate-200 rounded-[32px] overflow-hidden shadow-lg hover:shadow-2xl transition-all hover:scale-[1.03]"
+              >
+                <img
+                  src={img.url}
+                  alt={img.prompt}
+                  className="w-full h-auto object-cover"
+                />
+                <div className="absolute inset-0 bg-gradient-to-t from-[#004ea8]/90 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-all p-8 flex flex-col justify-end">
+                  <div className="bg-white/10 backdrop-blur-xl p-4 rounded-2xl border border-white/20">
+                    <p className="text-sm text-white font-semibold line-clamp-2">
+                      "{img.prompt}"
+                    </p>
+                    <a
+                      href={img.url}
+                      download={`asset-${i}.png`}
+                      className="mt-4 block w-full bg-white text-[#004ea8] text-center py-3 rounded-xl text-xs font-black uppercase tracking-widest transition-colors hover:bg-slate-100"
+                    >
+                      Export Resolution
+                    </a>
+                  </div>
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ImageView;

+ 107 - 0
EsimLao/esim-vite/src/components/Popup.tsx

@@ -0,0 +1,107 @@
+import React from "react";
+import { useAppDispatch, useAppSelector } from "../hooks/useRedux";
+import { closePopup } from "../features/popup/popupSlice";
+import { useNavigate } from "react-router-dom";
+
+const Popup = () => {
+  const popup = useAppSelector((state) => state.popup.notification);
+  const dispatch = useAppDispatch();
+  const navigate = useNavigate();
+  if (!popup.isOpen) return null;
+  return (
+    <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 animate-in fade-in duration-200">
+      {/* Backdrop */}
+      <div
+        className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
+        onClick={() => {
+          dispatch(closePopup());
+        }}
+      ></div>
+
+      {/* Modal Content */}
+      <div className="relative bg-white rounded-[40px] p-8 md:p-10 w-full max-w-sm shadow-2xl transform transition-all animate-in zoom-in-95 duration-200 scale-100 flex flex-col items-center text-center">
+        {/* Icon */}
+        <div
+          className={`w-20 h-20 rounded-full flex items-center justify-center mb-6 shadow-lg ${
+            popup.isSuccess
+              ? "bg-green-50 text-[#00c087]"
+              : "bg-red-50 text-[#EE0434]"
+          }`}
+        >
+          {popup.isSuccess ? (
+            <svg
+              className="w-10 h-10"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                strokeLinecap="round"
+                strokeLinejoin="round"
+                strokeWidth={3}
+                d="M5 13l4 4L19 7"
+              />
+            </svg>
+          ) : (
+            <svg
+              className="w-10 h-10"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                strokeLinecap="round"
+                strokeLinejoin="round"
+                strokeWidth={3}
+                d="M6 18L18 6M6 6l12 12"
+              />
+            </svg>
+          )}
+        </div>
+
+        {/* Text */}
+        <h3
+          className={`text-2xl font-black mb-3 tracking-tight ${
+            popup.isSuccess ? "text-slate-900" : "text-[#EE0434]"
+          }`}
+        >
+          {popup.title}
+        </h3>
+        <p className="text-slate-500 font-medium leading-relaxed mb-8">
+          {popup.message}
+        </p>
+
+        {/* Button */}
+        <div className="w-full flex flex-col md:flex-row gap-1 md:gap-1">
+          {popup.hasRightButton && (
+            <button
+              onClick={() => {
+                if (popup.rightButtonAction == "OPEN_ORDER_HISTORY") {
+                  navigate("/order-history");
+                }
+                dispatch(closePopup());
+              }}
+              className="mr-4 w-full py-4 rounded-2xl font-black text-xm shadow-lg active:scale-95 transition-all bg-[#EE0434] hover:bg-[#d10029] text-white shadow-red-200 p-2"
+            >
+              {popup.rightButtonText}
+            </button>
+          )}
+          <button
+            onClick={() => {
+              dispatch(closePopup());
+            }}
+            className={`w-full py-4 rounded-2xl font-black text-xm shadow-lg active:scale-95 transition-all p-2 ${
+              popup.isSuccess
+                ? "bg-[#00c087] hover:bg-[#00a876] text-white shadow-green-200"
+                : "bg-[#EE0434] hover:bg-[#d10029] text-white shadow-red-200"
+            }`}
+          >
+            {popup.buttonText}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Popup;

+ 50 - 0
EsimLao/esim-vite/src/components/ProductCard.tsx

@@ -0,0 +1,50 @@
+import React from "react";
+
+import { SimProduct } from "../services/types";
+import { Area } from "../services/product/type";
+import { useTranslation } from "react-i18next";
+import { formatCurrency, formatNumber } from "../logic/loigicUtils";
+
+const ProductCard: React.FC<{
+  p: Area;
+  onClick: (p: Area) => void;
+}> = ({ p, onClick }) => {
+  const { t } = useTranslation();
+  return (
+    <div
+      onClick={() => onClick(p)}
+      className="bg-white border border-slate-100 rounded-[20px] p-3 md:p-6 shadow-sm hover:shadow-lg transition-all relative flex flex-col md:flex-row items-center md:space-x-4 space-y-2 md:space-y-0 group cursor-pointer text-center md:text-left"
+    >
+      {p.promotionPercent !== 0 && (
+        <div className="absolute top-0 right-0 bg-[#EE0434] text-white text-[10px] font-black px-2 py-1 md:px-3 md:py-1.5 rounded-tr-[19px] rounded-bl-[14px] absolute-right-18">
+          {p.promotionPercent}%
+        </div>
+      )}
+      <div className="w-10 h-10 md:w-12 md:h-12 rounded-full overflow-hidden shrink-0 border border-slate-50 bg-slate-50">
+        <img
+          src={`${p.iconUrl}`}
+          alt={p.areaName1}
+          className="w-full h-full object-cover scale-150"
+        />
+      </div>
+      <div className="flex-1 w-full">
+        <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">
+          {formatCurrency(p.minDisplayPrice, p.curency)}
+        </p>
+        <p className="text-[#EE0434] font-bold text-xs md:text-lg">
+          <span className="text-black text-sm md:text-sm lg:text-base">
+            {t("from")}:{" "}
+          </span>
+          <span className="font-black text-lg md:text-xs lg:text-xl">
+            {formatCurrency(p.minSellPrice, p.curency)}
+          </span>
+        </p>
+      </div>
+    </div>
+  );
+};
+
+export default ProductCard;

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

@@ -0,0 +1,402 @@
+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:
+        "Bạn sẽ nhận được email chứa eSIM trong vòng 5 phút sau khi thanh toán thành công. ",
+      // 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:
+        //         0: Một ngày được tính đến 23:59 Giờ địa phương
+        // 1:Gói được tính theo   giờ Trung Quốc (UTC+8), nhưng sẽ reset vào 0:00 theo giờ quốc tế UTC+0.
+        // 2: Gói được tính theo ngày (0h–24h) của múi giờ quốc gia nơi gói cước được kích hoạt.
+        // 3: Một ngày được tính đến 23:59 giờ Bắc Kinh (GMT+8).
+        // 4: Thời gian tính theo múi giờ UTC+0, reset lúc 0:00.
+        // 5: Một ngày được tính đến 23:59 giờ Bắc Kinh (GMT+8)
+        // 6: Thời gian tính theo ngày (0h–24h) của múi giờ quốc gia nơi kích hoạt gói.
+        packageInfo?.dataResetTime === 0
+          ? "Một ngày được tính đến 23:59 Giờ địa phương."
+          : packageInfo?.dataResetTime === 1
+            ? "Gói được tính theo   giờ Trung Quốc (UTC+8), nhưng sẽ reset vào 0:00 theo giờ quốc tế UTC+0."
+            : packageInfo?.dataResetTime === 2
+              ? "Gói được tính theo ngày (0h–24h) của múi giờ quốc gia nơi gói cước được kích hoạt."
+              : packageInfo?.dataResetTime === 3
+                ? "Một ngày được tính đến 23:59 giờ Bắc Kinh (GMT+8)."
+                : packageInfo?.dataResetTime === 4
+                  ? "Thời gian tính theo múi giờ UTC+0, reset lúc 0:00."
+                  : packageInfo?.dataResetTime === 5
+                    ? "Một ngày được tính đến 23:59 giờ Bắc Kinh (GMT+8)."
+                    : packageInfo?.dataResetTime === 6
+                      ? "Thời gian tính theo ngày (0h–24h) của múi giờ quốc gia nơi kích hoạt gói."
+                      : "",
+      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-800">
+            {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>{" "}
+                {/* display html */}
+                <span dangerouslySetInnerHTML={{ __html: 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;

+ 54 - 0
EsimLao/esim-vite/src/components/QRCodeModal.tsx

@@ -0,0 +1,54 @@
+import React from "react";
+import { useAppDispatch, useAppSelector } from "../hooks/useRedux";
+import { closeQRModal } from "../features/popup/popupSlice";
+
+const QRCodeModal = () => {
+  const popup = useAppSelector((state) => state.popup.qrModal);
+  const dispatch = useAppDispatch();
+  if (popup.isOpen === false) {
+    return null;
+  }
+
+  return (
+    <div className="fixed inset-0 z-[150] flex items-center justify-center p-4 animate-in fade-in duration-200">
+      {/* Backdrop */}
+      <div
+        className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"
+        onClick={() => {
+          dispatch(closeQRModal());
+        }}
+      ></div>
+
+      {/* Modal Container */}
+      <div className="relative bg-white rounded-[32px] w-full max-w-[340px] shadow-2xl transform transition-all animate-in zoom-in-95 duration-200 overflow-hidden flex flex-col">
+        <div className="p-8 pb-6 text-center">
+          <h3 className="text-xl md:text-2xl font-black text-slate-900 mb-8 tracking-tight">
+            Mã QR eSIM
+          </h3>
+
+          <div className="bg-slate-50 p-4 rounded-[24px] mb-8 border border-slate-100">
+            <div className="aspect-square w-full bg-white rounded-xl flex items-center justify-center p-3 shadow-sm">
+              {/* Placeholder QR Code for demo purposes */}
+              <img
+                src={popup.qrData}
+                alt="eSIM QR Code"
+                className="w-full h-full object-contain mix-blend-multiply"
+              />
+            </div>
+          </div>
+
+          <button
+            onClick={() => {
+              dispatch(closeQRModal());
+            }}
+            className="w-full py-3.5 rounded-xl font-bold text-white bg-[#EE0434] hover:bg-[#EE0434] active:scale-[0.98] transition-all shadow-lg shadow-cyan-100/50 text-base md:text-lg"
+          >
+            Đóng
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default QRCodeModal;

+ 18 - 0
EsimLao/esim-vite/src/components/ScrollToTop.tsx

@@ -0,0 +1,18 @@
+import { useEffect } from "react";
+import { useLocation } from "react-router-dom";
+
+const ScrollToTop = () => {
+  const { pathname } = useLocation();
+
+  useEffect(() => {
+    window.scrollTo({
+      top: 0,
+      left: 0,
+      behavior: "auto", // khuyến nghị: auto cho performance
+    });
+  }, [pathname]);
+
+  return null;
+};
+
+export default ScrollToTop;

+ 121 - 0
EsimLao/esim-vite/src/components/SearchView.tsx

@@ -0,0 +1,121 @@
+import React, { useState } from "react";
+import { searchGrounding } from "../services/gemini";
+import { GroundingSource } from "../services/types";
+import { useTranslation } from "react-i18next";
+
+const SearchView: React.FC = () => {
+  const [query, setQuery] = useState("");
+  const [loading, setLoading] = useState(false);
+  const [result, setResult] = useState<{
+    text: string;
+    sources: GroundingSource[];
+  } | null>(null);
+  const { t } = useTranslation();
+
+  const handleSearch = async () => {
+    if (!query.trim() || loading) return;
+    setLoading(true);
+    setResult(null);
+    try {
+      const data = await searchGrounding(query);
+      setResult(data);
+    } catch (error) {
+      console.error(error);
+      alert("Search failed. Try a different query.");
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="flex flex-col h-full bg-gray-950">
+      <div className="p-12 max-w-4xl mx-auto w-full">
+        <div className="text-center mb-12">
+          <h2 className="text-4xl font-extrabold mb-4 bg-gradient-to-r from-blue-400 to-emerald-400 bg-clip-text text-transparent">
+            {t("insightEngine")}
+          </h2>
+          <p className="text-gray-400 text-lg">
+            {t("realTimeWebSearchPoweredByGoogleGrounding")}
+          </p>
+        </div>
+
+        <div className="relative group">
+          <input
+            type="text"
+            value={query}
+            onChange={(e) => setQuery(e.target.value)}
+            onKeyDown={(e) => e.key === "Enter" && handleSearch()}
+            placeholder="What are the major highlights of the tech industry today?"
+            className="w-full bg-gray-900/50 text-xl text-gray-100 rounded-3xl px-8 py-6 focus:outline-none focus:ring-4 focus:ring-blue-500/20 border-2 border-gray-800 group-hover:border-gray-700 transition-all placeholder-gray-700 shadow-2xl"
+          />
+          <button
+            onClick={handleSearch}
+            disabled={loading}
+            className="absolute right-4 top-4 bottom-4 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-800 text-white rounded-2xl px-8 font-bold transition-colors shadow-lg"
+          >
+            {loading ? "Thinking..." : "Search"}
+          </button>
+        </div>
+
+        {loading && (
+          <div className="mt-20 flex flex-col items-center space-y-4">
+            <div className="flex space-x-2">
+              <div className="w-3 h-3 bg-blue-500 rounded-full animate-ping"></div>
+              <div className="w-3 h-3 bg-blue-500 rounded-full animate-ping [animation-delay:0.2s]"></div>
+              <div className="w-3 h-3 bg-blue-500 rounded-full animate-ping [animation-delay:0.4s]"></div>
+            </div>
+            <p className="text-gray-500 animate-pulse font-medium">
+              {t("scouringTheWebForRealTimeInformation")}
+            </p>
+          </div>
+        )}
+
+        {result && (
+          <div className="mt-12 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
+            <div className="bg-gray-900/80 border border-gray-800 p-8 rounded-3xl shadow-xl leading-relaxed text-gray-200">
+              <div className="prose prose-invert max-w-none">
+                <p className="whitespace-pre-wrap text-lg">{result.text}</p>
+              </div>
+            </div>
+
+            {result.sources.length > 0 && (
+              <div className="space-y-3">
+                <h3 className="text-sm font-bold uppercase tracking-widest text-gray-500 px-2">
+                  Verified Sources
+                </h3>
+                <div className="flex flex-wrap gap-3">
+                  {result.sources.map((source, i) => (
+                    <a
+                      key={i}
+                      href={source.uri}
+                      target="_blank"
+                      rel="noopener noreferrer"
+                      className="bg-gray-900 border border-gray-800 hover:border-blue-500/50 hover:bg-gray-800 px-4 py-2 rounded-xl text-sm text-gray-300 transition-all flex items-center space-x-2"
+                    >
+                      <svg
+                        className="w-4 h-4 text-blue-400"
+                        fill="none"
+                        stroke="currentColor"
+                        viewBox="0 0 24 24"
+                      >
+                        <path
+                          strokeLinecap="round"
+                          strokeLinejoin="round"
+                          strokeWidth={2}
+                          d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
+                        />
+                      </svg>
+                      <span className="font-medium">{source.title}</span>
+                    </a>
+                  ))}
+                </div>
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default SearchView;

+ 69 - 0
EsimLao/esim-vite/src/components/Sidebar.tsx

@@ -0,0 +1,69 @@
+
+import React from 'react';
+import { useLocation, Link } from 'react-router-dom';
+
+const Sidebar: React.FC = () => {
+  const location = useLocation();
+  const path = location.pathname;
+
+  const menuItems = [
+    { id: '/ai/chat', label: 'Intelligent Chat', icon: 'M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z' },
+    { id: '/ai/image', label: 'Image Forge', icon: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z' },
+    { id: '/ai/video', label: 'Cinematic Video', icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z' },
+    { id: '/ai/search', label: 'Knowledge Base', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
+  ];
+
+  return (
+    <div className="w-72 bg-white border-r border-slate-200 flex flex-col h-full z-20 shadow-xl">
+      <div className="p-8">
+        <h1 className="text-2xl font-black text-[#EE0434] tracking-tight">
+          LUMINA
+        </h1>
+        <div className="h-1 w-12 bg-[#EE0434] mt-1 rounded-full"></div>
+        <p className="text-[10px] text-slate-400 mt-3 uppercase tracking-[0.2em] font-bold">Solutions Platform</p>
+      </div>
+
+      <nav className="flex-1 px-4 space-y-1 mt-2">
+        {menuItems.map((item) => (
+          <Link
+            key={item.id}
+            to={item.id}
+            className={`w-full flex items-center space-x-3 px-6 py-4 rounded-xl transition-all duration-300 ${
+              path === item.id
+                ? 'bg-red-50 text-[#EE0434] font-semibold shadow-inner'
+                : 'text-slate-500 hover:bg-red-50/50 hover:text-[#EE0434]'
+            }`}
+          >
+            <svg
+              className={`w-6 h-6 transition-colors ${path === item.id ? 'text-[#EE0434]' : 'text-slate-300'}`}
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+            >
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
+            </svg>
+            <span className="text-sm">{item.label}</span>
+            {path === item.id && (
+                <div className="ml-auto w-1.5 h-6 bg-[#EE0434] rounded-full"></div>
+            )}
+          </Link>
+        ))}
+      </nav>
+
+      <div className="p-6 mt-auto">
+        <div className="bg-slate-50 p-4 rounded-2xl border border-slate-100 flex items-center space-x-3 shadow-sm hover:shadow-md transition-shadow cursor-pointer">
+          <div className="w-10 h-10 rounded-xl bg-[#EE0434] flex items-center justify-center text-white font-bold shadow-lg">
+            S
+          </div>
+          <div className="flex-1 min-w-0">
+            <p className="text-sm font-bold text-slate-800 truncate">Senior Executive</p>
+            <p className="text-[10px] text-[#EE0434] font-bold uppercase tracking-wider">Enterprise Pro</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Sidebar;

+ 37 - 0
EsimLao/esim-vite/src/components/TestimonialCard.tsx

@@ -0,0 +1,37 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { TestimonialType } from "../services/types";
+import { Review } from "../services/content/types";
+
+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.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.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) => (
+        <svg
+          key={i}
+          className="w-3.5 h-3.5 md:w-4 md:h-4 text-yellow-400"
+          fill="currentColor"
+          viewBox="0 0 20 20"
+        >
+          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
+        </svg>
+      ))}
+    </div>
+    <p className="text-xs md:text-sm font-medium leading-relaxed italic text-white/90 whitespace-normal">
+      "{item.reviewContent}"
+    </p>
+  </div>
+);
+export default TestimonialCard;

+ 35 - 0
EsimLao/esim-vite/src/components/TopLoader.tsx

@@ -0,0 +1,35 @@
+import React from "react";
+import { useAppSelector } from "../hooks/useRedux";
+
+const TopLoader = () => {
+  const isLoading = useAppSelector((state) => state.loading.isLoading);
+  return (
+    isLoading && (
+      <div
+        className={`fixed top-0 left-0 w-full h-[3px] z-[9999] pointer-events-none transition-opacity duration-500 ${
+          isLoading ? "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]" />
+
+        <style>{`
+        @keyframes progress-fill {
+          0% { width: 0%; left: 0%; }
+          50% { width: 70%; left: 15%; }
+          100% { width: 0%; left: 100%; }
+        }
+        @keyframes shimmer-flow {
+          0% { transform: translateX(-100%); }
+          100% { transform: translateX(200%); }
+        }
+      `}</style>
+      </div>
+    )
+  );
+};
+
+export default TopLoader;

+ 191 - 0
EsimLao/esim-vite/src/components/VideoView.tsx

@@ -0,0 +1,191 @@
+import React, { useState, useEffect } from "react";
+import { generateVideo } from "../services/gemini";
+import { GeneratedVideo } from "../services/types";
+
+const VideoView: React.FC = () => {
+  const [prompt, setPrompt] = useState("");
+  const [loading, setLoading] = useState(false);
+  const [history, setHistory] = useState<GeneratedVideo[]>([]);
+  const [hasKey, setHasKey] = useState(false);
+  const [aspectRatio, setAspectRatio] = useState<"16:9" | "9:16">("16:9");
+
+  useEffect(() => {
+    const checkKey = async () => {
+      // @ts-ignore
+      const ok = await window.aistudio.hasSelectedApiKey();
+      setHasKey(ok);
+    };
+    checkKey();
+  }, []);
+
+  const handleSelectKey = async () => {
+    // @ts-ignore
+    await window.aistudio.openSelectKey();
+    setHasKey(true);
+  };
+
+  const handleGenerate = async () => {
+    if (!prompt.trim() || loading) return;
+    setLoading(true);
+    try {
+      const url = await generateVideo(prompt, aspectRatio);
+      setHistory((prev) => [{ url, prompt, timestamp: new Date() }, ...prev]);
+    } catch (error) {
+      console.error(error);
+      alert(
+        "Video generation encountered an error. Veo might be busy or key issues."
+      );
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!hasKey) {
+    return (
+      <div className="flex flex-col items-center justify-center h-full bg-gray-950 p-12 text-center">
+        <div className="w-20 h-20 bg-yellow-500/10 rounded-full flex items-center justify-center mb-6">
+          <svg
+            className="w-10 h-10 text-yellow-500"
+            fill="none"
+            stroke="currentColor"
+            viewBox="0 0 24 24"
+          >
+            <path
+              strokeLinecap="round"
+              strokeLinejoin="round"
+              strokeWidth={2}
+              d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
+            />
+          </svg>
+        </div>
+        <h2 className="text-3xl font-bold mb-4">Activation Required</h2>
+        <p className="text-gray-400 max-w-md mb-8">
+          To access the professional-grade Veo 3.1 video engine, you must select
+          an API key from a billing-enabled Google Cloud project.
+        </p>
+        <div className="flex flex-col space-y-4">
+          <button
+            onClick={handleSelectKey}
+            className="bg-blue-600 hover:bg-blue-500 text-white px-8 py-3 rounded-xl font-bold transition-all shadow-xl"
+          >
+            Select Paid API Key
+          </button>
+          <a
+            href="https://ai.google.dev/gemini-api/docs/billing"
+            target="_blank"
+            className="text-sm text-blue-400 hover:underline"
+          >
+            Learn about billing →
+          </a>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="flex flex-col h-full bg-gray-950">
+      <div className="p-8 border-b border-gray-900 flex flex-col space-y-4">
+        <div className="flex items-center justify-between">
+          <h2 className="text-2xl font-bold">Cinematic Video</h2>
+          <span className="bg-indigo-500/10 text-indigo-400 text-[10px] uppercase tracking-widest px-2 py-1 rounded font-bold border border-indigo-500/20">
+            Veo 3.1 Pro
+          </span>
+        </div>
+        <div className="flex flex-col md:flex-row gap-4">
+          <textarea
+            rows={2}
+            value={prompt}
+            onChange={(e) => setPrompt(e.target.value)}
+            placeholder="A slow-motion tracking shot of a silver jaguar running through a dark neon forest..."
+            className="flex-1 bg-gray-900 text-gray-100 rounded-xl px-5 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500/50 border border-gray-800 resize-none"
+          />
+          <div className="flex flex-col space-y-2">
+            <select
+              value={aspectRatio}
+              onChange={(e: any) => setAspectRatio(e.target.value)}
+              className="bg-gray-900 text-gray-200 border border-gray-800 rounded-xl px-4 py-2 outline-none"
+            >
+              <option value="16:9">Landscape (16:9)</option>
+              <option value="9:16">Portrait (9:16)</option>
+            </select>
+            <button
+              onClick={handleGenerate}
+              disabled={loading}
+              className="flex-1 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 disabled:opacity-50 text-white rounded-xl px-8 py-3 font-semibold transition-all shadow-lg"
+            >
+              {loading ? "Synthesizing..." : "Generate 720p"}
+            </button>
+          </div>
+        </div>
+        {loading && (
+          <div className="bg-gray-900 border border-gray-800 rounded-xl p-4 animate-pulse flex items-center space-x-4">
+            <div className="w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
+            <div>
+              <p className="text-sm font-medium text-gray-300">
+                Wait, magic in progress...
+              </p>
+              <p className="text-xs text-gray-500">
+                Video generation typically takes 2-4 minutes. Do not close this
+                tab.
+              </p>
+            </div>
+          </div>
+        )}
+      </div>
+
+      <div className="flex-1 overflow-y-auto p-8 grid grid-cols-1 xl:grid-cols-2 gap-8">
+        {history.map((video, idx) => (
+          <div
+            key={idx}
+            className="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden shadow-2xl"
+          >
+            <video
+              controls
+              src={video.url}
+              className="w-full h-auto aspect-video bg-black"
+            />
+            <div className="p-4">
+              <p className="text-sm text-gray-400 font-medium line-clamp-2">
+                "{video.prompt}"
+              </p>
+              <div className="mt-4 flex justify-between items-center">
+                <span className="text-xs text-gray-600">
+                  {video.timestamp.toLocaleDateString()}
+                </span>
+                <a
+                  href={video.url}
+                  download
+                  className="text-xs text-blue-400 font-bold hover:underline"
+                >
+                  Export MP4
+                </a>
+              </div>
+            </div>
+          </div>
+        ))}
+        {history.length === 0 && !loading && (
+          <div className="col-span-full h-full flex flex-col items-center justify-center opacity-20 py-20">
+            <svg
+              className="w-24 h-24"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                strokeLinecap="round"
+                strokeLinejoin="round"
+                strokeWidth={1}
+                d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
+              />
+            </svg>
+            <p className="mt-4 text-xl font-medium">
+              Ready to produce professional cinema?
+            </p>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default VideoView;

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

@@ -0,0 +1,35 @@
+import { setWithExpiry } from "../../logic/loigicUtils";
+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>) => {
+      setWithExpiry("token", action.payload.accessToken);
+      setWithExpiry("refreshToken", action.payload.refreshToken);
+      setWithExpiry("accountInfo", action.payload);
+      return {
+        ...state,
+        account: action.payload,
+      };
+    },
+    accountLogout: (state) => {
+      localStorage.removeItem("token");
+      localStorage.removeItem("refreshToken");
+      localStorage.removeItem("accountInfo");
+      localStorage.removeItem("selectedPartner");
+      localStorage.removeItem("selectedLanguage");
+      return {
+        ...state,
+        account: null,
+      };
+    },
+  },
+});
+
+export const { accountLogin, accountLogout } = accountSlice.actions;
+export default accountSlice.reducer;

+ 22 - 0
EsimLao/esim-vite/src/features/areas/areasSlice.ts

@@ -0,0 +1,22 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import loadingSlice from "../loading/loadingSlice";
+import { Area } from "../../services/product/type";
+
+const areaSlice = createSlice({
+  name: "areas",
+  initialState: {
+    areas: [] as Area[],
+  },
+  reducers: {
+    setAreas: (state, action: PayloadAction<Area[]>) => {
+      console.log("Setting areas state to: ", action.payload);
+      return {
+        ...state,
+        areas: action.payload,
+      };
+    },
+  },
+});
+
+export const { setAreas } = areaSlice.actions;
+export default areaSlice.reducer;

+ 37 - 0
EsimLao/esim-vite/src/features/loading/loadingSlice.ts

@@ -0,0 +1,37 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+const loadingSlice = createSlice({
+  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 ...",
+      };
+    },
+    stopLoading: (state) => {
+      state.isLoading = false;
+    },
+    startSmallLoading: (state) => {
+      state.isSmallLoading = true;
+    },
+    stopSmallLoading: (state) => {
+      state.isSmallLoading = false;
+    },
+  },
+});
+
+export const {
+  startLoading,
+  stopLoading,
+  startSmallLoading,
+  stopSmallLoading,
+} = loadingSlice.actions;
+export default loadingSlice.reducer;

+ 68 - 0
EsimLao/esim-vite/src/features/popup/popupSlice.ts

@@ -0,0 +1,68 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import loadingSlice from "../loading/loadingSlice";
+
+const popupSlice = createSlice({
+  name: "popup",
+  initialState: {
+    notification: {
+      isOpen: false,
+      isSuccess: true,
+      title: "Notification",
+      message: "",
+      buttonText: "Close",
+      hasRightButton: false,
+      rightButtonText: "Confirm",
+      rightButtonAction: "",
+    },
+    compatibilityModal: {
+      isOpen: false,
+    },
+    qrModal: {
+      isOpen: false,
+      qrData: "",
+    },
+  },
+  reducers: {
+    openPopup: (state, action: PayloadAction<any>) => {
+      return {
+        ...state,
+        notification: {
+          isOpen: true,
+          isSuccess: action.payload?.isSuccess ?? true,
+          title: action.payload?.title ?? "Notification",
+          message: action.payload?.message,
+          buttonText: action.payload?.buttonText ?? "Close",
+          hasRightButton: action.payload?.hasRightButton ?? false,
+          rightButtonText: action.payload?.rightButtonText ?? "Confirm",
+          rightButtonAction: action.payload?.rightButtonAction ?? "",
+        },
+      };
+    },
+    closePopup: (state) => {
+      state.notification.isOpen = false;
+    },
+    openCompatibilityModal: (state) => {
+      state.compatibilityModal.isOpen = true;
+    },
+    closeCompatibilityModal: (state) => {
+      state.compatibilityModal.isOpen = false;
+    },
+    openQRModal: (state, action: PayloadAction<string>) => {
+      state.qrModal.isOpen = true;
+      state.qrModal.qrData = action.payload;
+    },
+    closeQRModal: (state) => {
+      state.qrModal.isOpen = false;
+    },
+  },
+});
+
+export const {
+  openPopup,
+  closePopup,
+  openCompatibilityModal,
+  closeCompatibilityModal,
+  openQRModal,
+  closeQRModal,
+} = popupSlice.actions;
+export default popupSlice.reducer;

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

@@ -0,0 +1,21 @@
+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",
+  FAQS_HEADER: "faqs_header",
+  PACKAGES: "packages",
+  PAYMENT_CHANNELS: "payment_channels",
+  FAQ_GUIDES: "faq_guides",
+  AREAS_FOOTER: "areas_footer",
+};

+ 5 - 0
EsimLao/esim-vite/src/hooks/useRedux.ts

@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { RootState, AppDispatch } from '../app/store';
+
+export const useAppDispatch = () => useDispatch<AppDispatch>();
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

+ 20 - 0
EsimLao/esim-vite/src/i18n/index.ts

@@ -0,0 +1,20 @@
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+
+import en from "./locales/en.json";
+import vi from "./locales/vi.json";
+import la from "./locales/la.json";
+
+i18n.use(initReactI18next).init({
+  resources: {
+    la: { translation: la },
+    vi: { translation: vi },
+  },
+  lng: "vi", // default
+  fallbackLng: "vi",
+  interpolation: {
+    escapeValue: false,
+  },
+});
+
+export default i18n;

+ 230 - 0
EsimLao/esim-vite/src/i18n/locales/en.json

@@ -0,0 +1,230 @@
+{
+  "buySim": "Buy a SIM",
+  "viewAll": "View all",
+  "mostPopular": "Most Popular",
+  "region": "Region",
+  "guide": "Guide",
+  "whatEsim": "What is eSIM",
+  "installationInstructions": "Installation instructions",
+  "supportSupport": "Support",
+  "orderSearch": "Order Tracking Search",
+  "seeMore": "See more",
+  "whatUs": "What our customers say about us",
+  "chooseGetgo": "Why choose SkySimHub",
+  "fastCost": "Fast – Convenient – Reasonable Cost",
+  "globalAnywhere": "Global connection, Stable Anywhere",
+  "customerSupport": "24/7 Customer Support",
+  "customerPolicy": "Customer-first Policy",
+  "getgoWith": "has partnered with",
+  "buyExplore": "Buy a SIM easily, ready to explore",
+  "compatibleDevice": "Compatible Device",
+  "pickPlan": "Pick Your Plan",
+  "instantActivation": "Instant Activation",
+  "frequentlyQuestions": "Frequently Asked Questions",
+  "refundDefective": "100% refund policy if product is defective",
+  "returnGuide": "Return and Refund Guide",
+  "chooseTo": "Choose country you're going to",
+  "doesEsim?": "Does my phone support eSIM?",
+  "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",
+  "getgoGetgo": "SkySimHub",
+  "aboutUs": "About Us",
+  "termsService": "Terms of Service",
+  "privacyPolicy": "Privacy Policy",
+  "travelSim": "Travel SIM",
+  "chinaSim": "China SIM",
+  "thailandSim": "Thailand SIM",
+  "japanSim": "Japan SIM",
+  "emailAsia": "Email: klinhnguyen@viettech.asia",
+  "zaloWhatsapp": "Zalo/WhatsApp: +84336548007",
+  "verifiedSpeed": "Verified: High Speed",
+  "numberDays": "Number of days",
+  "dataData": "Data",
+  "unlimitedUnlimited": "Unlimited",
+  "simType": "SIM Type",
+  "quantity": "Quantity",
+  "esimEsim": "eSIM",
+  "days": "days",
+  "discountDiscount": "Discount",
+  "pricePrice": "Price",
+  "vnd": "VND",
+  "continueShopping": "Continue shopping",
+  "chooseProduct": "Choose Product",
+  "orderInformation": "Order Information",
+  "paymentPayment": "Payment",
+  "customerInformation": "Customer Information",
+  "lastName": "Last Name",
+  "firstName": "First Name",
+  "emailEmail": "Email",
+  "confirmEmail": "Confirm Email",
+  "phoneNumber": "Phone number",
+  "orderInfo": "Order Info",
+  "esim": "eSIM",
+  "paymentMethod": "Payment method",
+  "onepayOnepay": "OnePay",
+  "iService": "I have read and agree to the Terms of Service",
+  "iUnlocked": "I confirm that my device supports eSIM and has been network unlocked",
+  "iInvoice": "I want to issue a VAT Invoice",
+  "overviewOverview": "Overview",
+  "totalTotal": "Total",
+  "paymentStatus": "Payment Status",
+  "paymentSuccessful": "Payment successful!",
+  "viewOrder": "View Order",
+  "close": "Close",
+  "transactionHistory": "Transaction History",
+  "enterCode": "Enter order code",
+  "fromDate": "From date",
+  "toDate": "To date",
+  "statusStatus": "Status",
+  "successSuccess": "Success",
+  "pendingPayment": "Pending Payment",
+  "transactionNo.": "Transaction ID / Order No.",
+  "dateTime": "Date & Time",
+  "amountAmount": "Amount",
+  "detailDetail": "Detail",
+  "manageManage": "Manage",
+  "fullName": "Full Name",
+  "simNam": "SIM Viet Nam",
+  "validityPeriod": "Validity period",
+  "daysDays": "days",
+  "qrCode": "QR Code",
+  "getUs": "Get in touch with us",
+  "weArePossible": "We're always ready to listen and support you with any questions. Leave your information, and our team will get back to you as soon as possible.",
+  "enterName": "Enter your full name",
+  "specificRequirements": "Specific Requirements",
+  "describeRequirements": "Describe your requirements",
+  "submitSubmit": "Submit",
+  "from": "from",
+  "noMatchesFound": "No matches found",
+  "viewAllDestinations": "View All Destinations",
+  "selectLanguage": "Select Language",
+  "weAreAlwaysHereToSupportYou": "We are always here to support you.",
+  "frequently": "Frequently",
+  "askedQuestions": "Asked Questions",
+  "fastestData": "Fastest Data",
+  "bestPrices": "Best Prices",
+  "connectInstantlyInOver200Destinations": "Connect instantly in over 200 destinations",
+  "countryList": "Country List",
+  "regionList": "Regions",
+  "noCountryMatchesFound": "No countries found matching \"{searchQuery}\"",
+  "doesEsimWorkWithMyDevice": "Does my phone support eSIM?",
+  "sayAboutUs": "say about us",
+  "overMillionSatisfiedCustomers": "Over 1,000,000 satisfied customers have used our services",
+  "simCountries": "SIM Countries",
+  "apply": "Apply",
+  "subtotal": "Subtotal",
+  "checkout": "Checkout",
+  "getInTouch": "Get in touch",
+  "withUs": "with us",
+  "backToNews": "Back to News",
+  "myEsim": "My eSIM",
+  "back": "Back",
+  "failure": "Failure",
+  "guideAndHelp": "Guide & Help",
+  "selectTopicInstructions": "Select a topic from the left to view detailed instructions for your travel SIM or eSIM. Our support team is also available 24/7 via chat.",
+  "importantNote": "Important Note: Please make sure your device supports eSIM before purchasing. The information below is for reference purposes only. We are not responsible for any changes in device information. We will not refund any order if the device does not support eSIM, including the following cases:",
+  "someDualPhysicalSimModelsDoNotSupportEsim": "Some dual physical SIM phone models do not support eSIM.",
+  "carrierLockedDevicesMayNotSupportEsim": "Carrier-locked devices (contract-based subscription devices) may not support eSIM.",
+  "someModelsPurchasedInCertainCountriesMayNotSupportEsim": "Some models purchased in certain countries may not support eSIM (see table below).",
+  "toMakeSureYourDeviceSupportsEsim": "To make sure your device supports eSIM, please contact the store/provider where you purchased the device for the most accurate confirmation.",
+  "noteTheFollowingAppleDevicesDoNotSupportEsim": "*Note: The following apple devices do NOT support eSIM:",
+  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim": "All iPhones purchased from China, Hong Kong, and Macau DO NOT support eSIM.",
+  "method2CheckDirectlyOnYourDevice": "Method 2: Check directly on your device",
+  "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing": "Check if your device supports eSIM and whether it is carrier-locked by following",
+  "theStepsBelow": "the steps below",
+  "yourDeviceDoesNotSupportEsim": "Your device does not support eSIM?",
+  "tryFindingAPhysicalSimThatIsCompatibleWithYourDevice": "Try finding a physical SIM that is compatible with your device",
+  "here": "here",
+  "insightEngine": "Insight Engine",
+  "realTimeWebSearchPoweredByGoogleGrounding": "Real-time web search powered by Google Grounding",
+  "scouringTheWebForRealTimeInformation": "Scouring the web for real-time information...",
+  "news": "News",
+  "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 SkySimHub eSIM.",
+  "note2": "SkySimHub 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 SkySimHub.",
+  "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.",
+  "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",
+  "expiredAt": "Expired at",
+  "notActive": "Not Active",
+  "active": "Active",
+  "finished": "Finished",
+  "expired": "Expired",
+  "unknown": "Unknown",
+  "paymentSuccess": "Payment Successful",
+  "paymentFailed": "Payment Failed",
+  "activationMethod": "Activation Method",
+  "productDetailsMissing": "Please reload the page",
+  "goToShop": "Go to Shop",
+  "english": "English",
+  "vietnamese": "Vietnamese",
+  "numberOfDays": "Number of Days",
+  "verified": "Verified",
+  "highSpeed": "High Speed",
+  "selectADataPackageForYourDestination": "Select a data package for your destination.",
+  "checkIfYourPhoneSupportsESIMTechnology": "Check if your phone supports eSIM technology.",
+  "scanTheQRCodeAndConnectToHighSpeedData": "Scan the QR code and connect to high-speed data.",
+  "register": "Register"
+}

+ 231 - 0
EsimLao/esim-vite/src/i18n/locales/la.json

@@ -0,0 +1,231 @@
+{
+  "buySim": "ຊື້ຊີມ",
+  "viewAll": "ເບີ່ງທັ້ງໝົດ",
+  "mostPopular": "ຍອດນີຍົມທີ່ສຸດ",
+  "region": "ພື້ນທີ່",
+  "guide": "ນຳພາ",
+  "whatEsim": "eSim ແມ່ນຫຍັງ?",
+  "installationInstructions": "ແນະນຳການຕີດຕັ້ງ",
+  "supportSupport": "ການຊ່ວຍເຫຼືອ",
+  "orderSearch": "ກວດສອບການສັ່ງຊື້",
+  "seeMore": "ເບີ່ີ່ງເພີ່ມເຕີມ",
+  "whatUs": "ລູກຄ້າເວົ້າຫຍັງກ່ຽວກັບພວກເຮົາ",
+  "chooseGetgo": "ເປັນຫຍັງຈື່ງເລືອກ SkySimHub",
+  "fastCost": "ວ່ອງໄວ - ສະດວກ - ລາຄາສູດຄຸ້ມ",
+  "globalAnywhere": "ເຊື່ອມຕໍ່ທົ່ວໂລກ - ໝັ້ນຄົງທຸກທີ່",
+  "customerSupport": "ຊ່ວຍເຫຼືອລູກຄ້າ 24 ຊົ່ວໂມງ",
+  "customerPolicy": "ນະໂຍບາຍຄວາມສຳຄັນລູກຄ້າ",
+  "getgoWith": "ການຮ່ວມມືກັນ",
+  "buyExplore": "ຊື້ື້ SIM ງ່າຍດາຍ​ , ພ້ອມທີ່ຈະຄົ້ນຫາ",
+  "compatibleDevice": "ອຸປະກອນທີ່ເຂົ້າກັນໄດ້",
+  "pickPlan": "ເລືອກແພັກເກັດສີນຄ້າ",
+  "instantActivation": "ເປີດໃຊ້ທັນທີ",
+  "frequentlyQuestions": "ຄຳຖາມທີ່ມັກພົບ",
+  "refundDefective": "ຄືນເງີນ 100% ຖ້າສິນຄ້າມີບັນຫາ",
+  "returnGuide": "ຊ່ວຍເຫຼືອ ການປ່ຽນ ແລະ ສົ່ງຄືນເງີນ",
+  "chooseTo": "ເລືືອກປະເທດທີ່ທ່ານຈະໄປ",
+  "doesEsim?": "ໂທລະສັບຂອງຂ້ອຍມີຮອງຮັບ eSIM ບໍ?",
+  "orders": "ຂໍ້ມູນສິນຄ້າ",
+  "logout": "ອອກຈາກລະບົບ",
+  "viettechLimited": "ບໍໍລີສັດ TNHH ພັດທະນາທົ່ວໂລກ VIETTECH",
+  "taxCode": "ເລກພາສີ ; 0901210362",
+  "address": "218 ດາວເຊືອ 1 , ເຂດຕົວເມືອງ Vinhomes Ocean park 2,ຊຸມຊົນ ເງຍຈຸ , ແຂວງ ຮຶງອ່ຽນ, ຫວຽດນາມ",
+  "getgoGetgo": "SkySimHub",
+  "aboutUs": "ແນະນຳ",
+  "termsService": "ເງື່ອນໄຂການໃຫ້ບໍລິການ",
+  "privacyPolicy": "ນະໂຍບາຍຄວາມເປັນສ່ວນຕັວ",
+  "travelSim": "SIM ທ່ອງທ່ຽວ",
+  "chinaSim": "SIM ຈີນ",
+  "thailandSim": "SIM ໄທ",
+  "japanSim": "SIM ຍີ່ປຸ່ນ",
+  "emailAsia": "ອີເມວ: klinhnguyen@viettech.asia",
+  "zaloWhatsapp": "Zalo/Whatsapp: +84336548007",
+  "verifiedSpeed": "ໄດ້ກວດສອບ : ຄວາມໄວສູງ",
+  "numberDays": "ຈຳນວນມື້",
+  "dataData": "ຂໍ້ມູູນ",
+  "unlimitedUnlimited": "ບໍ່ຈຳກັດ",
+  "simType": "ປະເພດຊີມ",
+  "quantity": "ຈຳນວນ",
+  "esimEsim": "eSim",
+  "days": "ວັນທີ່",
+  "discountDiscount": "ຫຼຸດລາຄາ",
+  "pricePrice": "ລາຄາ",
+  "vnd": "ດົງ (VND) ຫວຽດນາມດົງ",
+  "continueShopping": "ສືບຕໍ່ຊື້ເຄື່ອງ",
+  "chooseProduct": "ເລືອກສິນຄ້າ",
+  "orderInformation": "ຂໍ້ມູນສິນຄ້າ",
+  "paymentPayment": "ຊຳລະເງີນ",
+  "customerInformation": "ຂໍ້ມູນລູກຄ້າ",
+  "lastName": "ນາມສະກຸນ",
+  "firstName": "ຊື່",
+  "emailEmail": "ອີເມວ",
+  "confirmEmail": "ຢືືນຢັນ ອີເມວ",
+  "phoneNumber": "ເບີໂທລະສັບ",
+  "orderInfo": "ຂໍ້ມູນການສັ່ງຊື້",
+  "esim": "eSIM",
+  "paymentMethod": "ວິທີການຊຳລະເງີນ",
+  "onepayOnepay": "OnePay",
+  "iService": "ຂ້ອຍໄດ້ອ່ານ ແລະ ເຫັນດີກັບເງື່ອນໄຂບໍລິການ",
+  "iUnlocked": "ຂ້ອຍຢືນຢັນວ່າອຸປະກອນຂອງຂ້ອຍຮອງຮັບ eSIM ແລະ ເຄື່ອຂ່າຍປົດລ໊ອກແລ້ວ",
+  "iInvoice": "ຂ້ອຍຕ້ອງການຮັບໃບແຈ້ງໝີ້ພາສີ",
+  "overviewOverview": "ພາບລວມ",
+  "totalTotal": "ລວມທັງໝົດ",
+  "paymentStatus": "ສະຖານະການຊຳລະເງີນ",
+  "paymentSuccessful": "ຊຳລະເງີີນສຳເລັດ!",
+  "viewOrder": "ເບິ່ງການສັ່ງຊື້",
+  "close": "ປິດ",
+  "transactionHistory": "ປະຫວັດການເຮັດທຸລະກຳ",
+  "enterCode": "ໃສ່ລະຫັດການສັ່ງຊື້",
+  "fromDate": "ຕັ້ງແຕ່ວັນທີ່",
+  "toDate": "ຮອດວັນທີ່",
+  "statusStatus": "ສະຖານະ",
+  "successSuccess": "ສຳເລັດ",
+  "pendingPayment": "ລໍຖ້າຊຳລະເງີນ",
+  "transactionNo.": "ລະຫັັດທຸລະກຳ / ລະຫັດການສັ່ງຊື້",
+  "dateTime": "ວັນທີ່ີ່ ແລະ ເວລາ",
+  "amountAmount": "ຈຳນວນເງີນ",
+  "detailDetail": "ລາຍລະອຽດ",
+  "manageManage": "ການຈັດການ",
+  "fullName": "ຊື່ ແລະ ນາມສະກຸນ",
+  "simNam": "SIM ຫວຽດນາມ",
+  "validityPeriod": "ໄລຍະການນຳໃຊ້",
+  "daysDays": "ວັນທີ່",
+  "qrCode": "ລະຫັດ QR",
+  "getUs": "ຕິດຕໍ່ຫາພວກເຮົາ",
+  "weArePossible": "ພວກເຮົາພ້ອມຈະຮັບຟັງ ແລະ ຊ່ວຍເຫຼືອທ່ານທຸກຢ່າງທີ່ສົງໄສ.ກະລຸນາຝາກຂໍ້ມູນໄວ້ ,ທີມງານພວກເຮົາຈະຕິດຕໍ່ກັບຫາທ່ານໄວທີ່ສຸດ",
+  "enterName": "ໃສ່ຊື່ ແລະ ນາມສະກຸນ ຂອງທ່ານ",
+  "specificRequirements": "ຈູດປະສົງຫຼັກ",
+  "describeRequirements": "ອະທີບາຍຄວາມຕ້ອງການຂອງທ່ານ",
+  "submitSubmit": "ສົ່ງ",
+  "from": "ຈາກ",
+  "noMatchesFound": "ບໍ່ພົບຜົນລັບ",
+  "viewAllDestinations": "ເບີ່ງຈຸດໝາຍປາຍທາງທັ້ງໝົດ",
+  "selectLanguage": "ເລືອກພາສາ",
+  "weAreAlwaysHereToSupportYou": "ພວກເຮົົາຢູ່ນີ້ສະເໝີເພື່ອຊ່ວຍເຫຼືອທ່ານ",
+  "frequently": "ສີ່ງທີ່ມັກພົບເຫັນ",
+  "askedQuestions": "ຄຳຖາມທີ່ີ່ໄດ້ຖາມ",
+  "fastestData": "ຂໍ້ມູນທີ່ໄວທີ່ສຸດ",
+  "bestPrices": "ລາຄາດີທີ່ສຸດ",
+  "connectInstantlyInOver200Destinations": "ການເຊື່ອມຕໍ່ທັນທີທີ່ຫລາຍກວ່າ 200 ຈຸດໝາຍປາຍທາງ",
+  "countryList": "ລາຍຊື່ແຕ່ລະປະເທດ",
+  "regionList": "ພື້້ນທີ່",
+  "noCountryMatchesFound": "ບໍ່ມີປະເທດໃດທີ່ເໝາະກັບ \" {searchQuery}\"",
+  "doesEsimWorkWithMyDevice": "ໂທລະສັັບຂອງຂ້ອຍມີການຮອງຮັບ eSIM ບໍ?",
+  "sayAboutUs": "ເວົ້າກ່ຽວກັບພວກເຮົາ",
+  "overMillionSatisfiedCustomers": "ຫຼາຍກວ່າ 1.000.000 ລູກຄ້າພໍໃຈທີ່ໄດ້ໃຊ້ບໍລີການພວກເຮົາ",
+  "simCountries": "ປະເທດ SIM",
+  "apply": "ນຳໃຊ້",
+  "subtotal": "ໂດຍປະມານ",
+  "checkout": "ການຊຳລະເງີນ",
+  "getInTouch": "ຕິດຕໍ່ໍ່",
+  "withUs": "ກັບພວກເຮົາ",
+  "backToNews": "ກັບຄືນໜ້າຫຼັກ",
+  "myEsim": "eSIM ຂອງຂ້ອຍ",
+  "back": "ກັບຄືືນ",
+  "failure": "ລົ້ມເຫຼວ",
+  "guideAndHelp": "ຄູ່ມື ແລະ ການຊ່ວນເຫຼືອ",
+  "selectTopicInstructions": "ເລືອກຫົວຂໍ້ແຕ່ເບື້ອງຊ້າຍ ເພື່ອເບິ່ງຄຳແນະນຳລາຍລະອຽດໃຫ້ SIM ທ່ອງທ່ຽວ ຫລື eSIM ຂອງທ່ານ.ທີມງານພວກເຮົາພ້ອມໃຫ້ຄຳປຶກສາ 24 ຊົ່ວໂມງ",
+  "importantNote": "",
+  "someDualPhysicalSimModelsDoNotSupportEsim": "ໂທລະສັບບາງລຸ້ນມີ 2 ຊີມ ບໍ່ຕ້ອງການ eSim",
+  "carrierLockedDevicesMayNotSupportEsim": "ອຸປະກອນທີ່ລ໊ອກໂດຍຜູ້ໃຫ້ບໍລິການ ( ອຸປະກອນທີ່ລົງທະບຽນພາຍໃຕ້ຕາມສັນຍາ) ອາດບໍ່ຮອງຮັບ eSIM.",
+  "someModelsPurchasedInCertainCountriesMayNotSupportEsim": "ບາງລຸ້ນທີ່ຊື່ໃນຕ່າງປະເທດອາດບໍ່ຮອງຮັບ eSIM ( ເບີ່ງຕາຕະລາງດ້ານລຸ່ມ)",
+  "toMakeSureYourDeviceSupportsEsim": "ເພື່ອແນ່ນອນວ່າອຸປະກອນຂອງທ່ານຮອງຮັບ eSIM, ກະລູນາຕິດຕໍ່ຫາທາງຮ້ານ/ບ່ອນທີ່ໃຫ້ທ່ານຊື້ອຸປະກອນເພື່ອກການຢືນຢັນທີ່ຖືກຕ້ອງທີ່ສຸດ.",
+  "noteTheFollowingAppleDevicesDoNotSupportEsim": "ໝາຍເຫດ : ອຸປະກອນ Apple ຕໍ່ໄປນີ້ບໍ່ຮອງຮັບ eSIM:",
+  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim": "iPhone ທີ່ຊື້ແຕ່ຈີນ, ຮອງກົງ ແລະ ມາເກົາ ບໍ່ຮອງຮັບ eSIM.",
+  "method2CheckDirectlyOnYourDevice": "ວິທີທີ່ 2 : ກວດສອບອຸປະກອນຂອງທ່ານໂດຍຕົງ",
+  "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing": "ກວດສອບດ້ວຍວິທີການກົດ *#06# ແລະ ໂທອອກ ,ຖ້າສະແດງ EID (eSIM ID) ແມ່ນອຸປະກອນໃຊ້ eSIM ໄດ້.",
+  "theStepsBelow": "ຂັ້ນຕອນດັ່ງລຸ່ມນີ້",
+  "yourDeviceDoesNotSupportEsim": "ອຸຸປະກອນຂອງທ່ານບໍ່ຮອງຮັບ eSIM ?",
+  "tryFindingAPhysicalSimThatIsCompatibleWithYourDevice": "ກະລຸນາລອງຄົ້ນຫາ SIM ກາດທີ່ເຂົ້າກັບອຸປະກອນທ່ານໄດ້",
+  "here": "ທີ່ນີ້",
+  "insightEngine": "ຄວາມເຂົ້າໃຈສຳລັບເຄື່ອງມື",
+  "realTimeWebSearchPoweredByGoogleGrounding": "ຄົ້ນຫາ Web ເວລານີ້ທີ່ຮອງຮັບຕົວຈິງໂດຍ Google Grounding",
+  "scouringTheWebForRealTimeInformation": "ກຳລັງຄົ້ນຫາຂໍ້ມູນແບບສົດໆໃນ Web...",
+  "news": "ຂ່າວສານ",
+  "contact": "ຕິດຕໍ່",
+  "home": "ໜ້າຫຼັກ",
+  "productInformation": "ຂໍ້ມູນສີນຄ້າ",
+  "networkProvider": "ຜູ້ໃຫ້ບໍລິການເຄືອຂ່າຍ",
+  "infoRegistration": "ລົງທະບຽນຂໍ້ມູນ",
+  "required": "ບໍ່ັງຄັບ",
+  "notRequired": "ບໍ່ບັງຄັບ",
+  "packageStartTime": "ເວລາເລີ່ມຕົ້ນແພັກເກັດ",
+  "usageTimeCalculatedFromSignal": "ການເປີດນຳໃຊ້ງານອັດຕະໂນມັດ, ການລົງຈອດແມ່ນໃຊ້ ( Plug and Play)",
+  "usageTimeCalculatedFromPurchase": "ເວລານຳໃຊ້ໄລ່ຕັ້ງແຕ່ຕອນຊື້ສິນຄ້າ",
+  "coverageArea": "ພື້ີນທີ່ຄວບຄຸມໃຫ້ບໍລິການ",
+  "deliveryMethod": "ວິທີການຈັດສົ່ງ",
+  "youWillReceiveAnEmailImmediatelyAfterPaymentWithEsimWaitForShippingAndReceiveYourPhysicalSim": "ທ່ານຈະໄດ້ຮັບຂໍ້ຄວາມທັນທີ່ພາຍຫຼັງຊຳລະເງີນກັບ eSIM. ລໍຖ້າຂົນສົ່ງແລະຮັບ SIM ທຳມະດາຂອງທ່ານ",
+  "physicalSimWillBeShippedToYourAddressWithin3-5BusinessDays": "SIM ທຳມະດາ ຈະໄດ້ສົ່ງໃຫ້ທ່ານໃນ 3 - 5 ມື້ ເຮັດວຽກ",
+  "wifiHotspot": "ຈຸດເຂົ້າເຖິງ Wi - Fi",
+  "yesSupported": "ມີ ( ຊ່ວຍເຫຼືອ )",
+  "no": "ບໍ່",
+  "networkTechnology": "ເຄືອຂ່າຍເທັກໂນໂລຢີ",
+  "dataResetTime": "ເວລາຕັ້ງຄ່າຂໍ້ມູນຄືນໃໝ່",
+  "timeResetInfo": "ໝື່ງມື້ໄລ່ຮອດ 23:59 ໂມງ ທ້ອງຖິ່ນ",
+  "unlimitedPolicy": "ແພັກເກັດບໍ່ມີກຳນົດ",
+  "appSupport": "ຊ່ວຍເຫຼືອ ແອັບພິເຄຊັນ",
+  "specifications": "ລາຍລະອຽດ",
+  "notes": "ໝາຍເຫດ",
+  "note1": "ຄຳແນະນຳການຕັ້ງຄ່າ eSIM ກ່ອນທີ່ຈະອອກເດີນທາງ (ຕ້ອງການເຊື່ອມຕໍ່ ອິນເຕີເນັດ ໃນການຕັ້ງຄ່າ), ເປີດອິນເຕີເນັດມືຖື ແລະ ປ່ຽນຈາກໃຊ້ອິນເຕີເນັດເປັນ eSIM SkySimHub.",
+  "note2": "SkySimHub ແນະນຳໃຫ້ຊື້ຢ່າງໜ່ອຍ 1 ມື້ ນອກເວລາຈູດປະສົງການນຳໃຊ້ຂອງທ່ານ ເພື່ອຫຼີກເວັ້ນວັນໝົດອາຍຸຂອງການບໍລິການນເນື່ອງຈາກຄວາມແຕກຕ່າງຂອງເຂດເວລາລະຫວ່າງປະເທດ",
+  "note3": "ແຕ່ລະ eSIM ສາມາດເປີດໃຊ້ງານໄດ້ພຽງເທື່ອດຽວ.",
+  "note4": "ເວລາທີ່ລໍຖ້າເປີດໃຊ້ງານ eSIM ແມ່ນ 30 ມື້ ເລີ່ມຕັ້ງແຕ່ການສັ່ງຊື້ສຳເລັດ.",
+  "note5": "ນະໂຍບາຍການຮັບປະກັນ : <br />+ ຮັບປະກັນການປ່ຽນແປງ ຫຼື ຄືນເງີນ 100% ໃນກໍລະນີມີບັນຫາທີ່ກ່ຽວຂ້ອງກັບຜູ້ໃຫ້ບໍລິການ <br / > ປ່ຽນແປງໃນເວລາ 1 ຊົ່ວໂມງ ຖ້າມັນເກີດຫຍັງຂື້ນໃນລະຫວ່າງການເປີດໃຊ້ງານ ຫຼື ນຳໃຊ້",
+  "note6": "ເຂົ້າເຖິງ TikTok ແລະ ChatGPT ອາດຈະມີຂໍ້ຈຳກັດຂຶ້ນກັບອຸປະກອນ, ລະບົດເຂົ້າກັນໄດ້ ແລະ ນະໂຍບາຍການຄວບຄຸມອິນເຕີເນັດທີ່ມີຢູ່ແລ້ວໃນປະດທດຈີນ",
+  "internationalTravelStarts": "ການທ່ອງທ່ຽວສາກົນເລີ່ມຕົ້ນພຽງແຕ່ 25.000​ VND, ປະຢັດໄປ 80% ທຽບກັບ Roaming ດັ້ງເດີມ.",
+  "easilyChooseSuitableSim": "ເລືອກ SIM ທີ່ເໝາະກັບຫຼາຍອຸປະກອນຢ່າງງ່າຍດາຍ",
+  "eSIMReceiveQR": "eSIM: ຮັບລະຫັດ QR ຜ່ານ Email, ສະແກນ ແລະ ຕັ້ງຄ່າ ພາຍໃນ 2 ນາທີ ເພື່ອເລີ່ມໃຊ້ທັນທີ່.",
+  "physicalSIMDelivery": "SIM ກາດ : ຈັດສົ່ງວ່ອງໄວທົ່ວໂລກ, ເຂົ້າກັນໄດ້ກັບທຸກອຸປະກອນ.",
+  "worldLeadingNetwork": "ໂຄງສ້າງພື້ນຖານເຄືອຂ່າຍຊັ້ນນຳຂອງໂລກ ຮັບປະກັນການເຊື່ອມຕໍ່ທີ່ໝັ້ນຄົງ ທຸກທີ່ ທູກເວລາ.",
+  "flexibleDiversePackages": "ແພັັກເກັດທີ່ມີຄວາມຢືດຫຍຸ໋ນ ແລະ ຫຼາກຫຼາຍ ຕອບສະໜອງຄວາມຕ້ອງການຂອງທ່ານ.",
+  "customerServiceAvailable": "ທີມງານດູແລລູກຄ້າຂອງພວກເຮົາພ້ອມໃຫ້ບໍລິການ 24 ຊົ່ວໂມງ ຕະຫຼອດການເດີນທາງຂອງທ່ານ.",
+  "supportChannels": "ຊ່ວຍໄດ້ຫລາຍຊ່ອງທາງ ເຊັ່ນ Hotline,Zalo OA ແລະ WhatsApp",
+  "commitmentPeaceOfMind": "ພວກເຮົົາສັນຍາທີ່ຈະໃຫ້ຄວາມເຊື່ອໝັ້ນ ແລະ ປະສົບການການບໍລິການຢ່າງໂປ່ງໃສ.",
+  "partnershipNetwork": "ຄູ່ຮ່ວມງານຢ່າງເປັນທາງການຂອງບໍລິສັດທົ່ວໂລກ",
+  "physicalSim": "SIM ທຳມະດາ",
+  "welcomeBack": "ຍັີນດີຕ້ອນຮັບທ່ານກັບມາ !",
+  "stayConnected": "ເຊື່ື່ອມໄດ້ທຸກທີ່ທຸກເວລາກັບ SkySimHub.",
+  "emailAddress": "ທີ່ຢູ່ Email",
+  "login": "ລົງທະບຽນ",
+  "orContinueWith": "ຫລື ສືບຕໍ່ດ້ວຍ...",
+  "checkYourEmail": "ກວດສອບ email ຂອງທ່ານ",
+  "weveSentVerificationCode": "ພວກເຮົາໄດ້ສົ່ງລະຫັດຢືນຢັນລວມ 6 ໂຕຈົນຮອດ..",
+  "verificationCode": "ລະຫັດຢືນຢັນ",
+  "codeExpiredPleaseRequestNewOne": "ລະຫັັດໄດ້ໝົດອາຍຸ. ກະລຸນາຂໍລະຫັດໃໝ່",
+  "fastSimpleRed": "ວ່ອງໄວ ງ່າຍດາຍ",
+  "globalConnectivityForTheModernTraveler": "ເຊື່ອມຕໍ່ທົ່ວໂລກໃຫ້ນັກທ່ອງທ່ຽວທັນສະໄໝ",
+  "codeExpiresIn": "ລະຫັດຈະໝົດອາຍູຫລັງຈາກນີ້",
+  "enterYourEmail": "ໃສ່ email ຂອງທ່ານ",
+  "searchCountry": "ຄົ້ນຫາປະເທດ...",
+  "suggestionsEsim": "ແນະນຳ eSIM ສຳລັບເສັ້ນທາງທ່ອງທ່ຽວຍອດນິຍົມ",
+  "support": "ຊ່ວຍເຫຼືອ",
+  "travelEsimSim": "eSIM / SIM ທ່ອງທ່ຽວ ແມ່ນຫຍັງ?",
+  "physicalTravelSim": "SIM ທ່ອງທ່ຽວທຳມະດາ",
+  "howToBuyTravelEsimSim": "ວິິທີຊື້ eSIM/SIM ທ່ອງທ່ຽວ",
+  "installationGuide": "ແນະນຳການຕັ້ັ້ງຄ່າ ແລະ ເປີດໃຊ້ງານ eSIM",
+  "emailAndEsimQrCode": "Email ແລະ ລະຫັດ QR eSIM",
+  "installationGuideForIphoneIos": "ແນະນຳຳການຕັ້ງຄ່າ iPhone ( iOS)",
+  "installationGuideForAndroid": "ແນະນຳການຕັ້ງຄ່າ Android",
+  "buyNow": "ຊື້ເລີຍ",
+  "expiredAt": "ໝົົດເວລາເຂົ້າ",
+  "notActive": "ຍັງບໍ່ເປີດນຳໃຊ້",
+  "active": "ກຳລັງເຄື່ອນໄຫວ",
+  "finished": "ນຳໃຊ້ໝົດແລ້ວ",
+  "expired": "ໝົດອາຍຸການນຳໃຊ້",
+  "unknown": "ບໍ່ກຳນົດ",
+  "paymentSuccess": "ການຊຳລະສຳເລັດ",
+  "paymentFailed": "ການຊຳລະລົ້ມເຫຼວ",
+  "activationMethod": "ວິທີການເປີດໃຊ້ງານ",
+  "productDetailsMissing": "ກະລຸນາດາວໂຫຼດໜ້າເວັບຄືນໃໝ່",
+  "goToShop": "ໄປທີ່ຮ້ານ",
+  "english": "ພາສາອັງກິດ",
+  "vietnamese": "ພາສາຫວຽດ",
+  "numberOfDays": "ຈຳນວນມື້",
+  "verified": "ຢືນຢັນແລ້ວ",
+  "highSpeed": "ຄວາມໄວສູງ",
+  "selectADataPackageForYourDestination": "ເລືອກແພັກເກັດອິນເຕີເນັດທີ່ເໝາະກັບປາຍທາງຂອງທ່ານ",
+  "checkIfYourPhoneSupportsESIMTechnology": "ກວດກາເບິ່ງໂທລະສັບຂອງທ່ານມີການຊ່ວຍເຫຼືອເທັກໂນໂລຢີ",
+  "scanTheQRCodeAndConnectToHighSpeedData": "ສະແກນ QR ແລະ ເຊື່ອມຕໍ່ກັບອິນເຕີເນັດຄວາມໄວສູງ",
+  "register": "ລົງທະບຽນ",
+  "lao": "ພາສາລາວ"
+}

+ 231 - 0
EsimLao/esim-vite/src/i18n/locales/vi.json

@@ -0,0 +1,231 @@
+{
+  "buySim": "Mua SIM",
+  "viewAll": "Xem tất cả",
+  "mostPopular": "Phổ biến nhất",
+  "region": "Khu vực",
+  "guide": "Hướng dẫn",
+  "whatEsim": "eSIM là gì",
+  "installationInstructions": "Hướng dẫn cài đặt",
+  "supportSupport": "Hỗ trợ",
+  "orderSearch": "Tra cứu đơn hàng",
+  "seeMore": "Xem thêm",
+  "whatUs": "Khách hàng nói gì về chúng tôi",
+  "chooseGetgo": "Vì sao chọn SkySimHub",
+  "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": "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",
+  "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?",
+  "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",
+  "getgoGetgo": "SkySimHub",
+  "aboutUs": "Giới thiệu",
+  "termsService": "Điều khoản dịch vụ",
+  "privacyPolicy": "Chính sách bảo mật",
+  "travelSim": "SIM du lịch",
+  "chinaSim": "SIM Trung Quốc",
+  "thailandSim": "SIM Thái Lan",
+  "japanSim": "SIM Nhật Bản",
+  "emailAsia": "Email: klinhnguyen@viettech.asia",
+  "zaloWhatsapp": "Zalo/Whatsapp: +84336548007",
+  "verifiedSpeed": "Đã xác thực: Tốc độ cao",
+  "numberDays": "Số ngày",
+  "dataData": "Dữ liệu",
+  "unlimitedUnlimited": "Không giới hạn",
+  "simType": "Loại SIM",
+  "quantity": "Số lượng",
+  "esimEsim": "eSIM",
+  "days": "ngày",
+  "discountDiscount": "Giảm giá",
+  "pricePrice": "Giá",
+  "vnd": "đ (VNĐ)",
+  "continueShopping": "Tiếp tục mua sắm",
+  "chooseProduct": "Chọn sản phẩm",
+  "orderInformation": "Thông tin đơn hàng",
+  "paymentPayment": "Thanh toán",
+  "customerInformation": "Thông tin khách hàng",
+  "lastName": "Họ",
+  "firstName": "Tên",
+  "emailEmail": "Email",
+  "confirmEmail": "Xác nhận Email",
+  "phoneNumber": "Số điện thoại",
+  "orderInfo": "Thông tin đơn hàng",
+  "esim": "eSIM",
+  "paymentMethod": "Phương thức thanh toán",
+  "onepayOnepay": "OnePay",
+  "iService": "Tôi đã đọc và đồng ý với Điều khoản dịch vụ",
+  "iUnlocked": "Tôi xác nhận thiết bị của mình có hỗ trợ eSIM và đã được mở khóa mạng",
+  "iInvoice": "Tôi muốn xuất hóa đơn VAT",
+  "overviewOverview": "Tổng quan",
+  "totalTotal": "Tổng cộng",
+  "paymentStatus": "Trạng thái thanh toán",
+  "paymentSuccessful": "Thanh toán thành công!",
+  "viewOrder": "Xem đơn hàng",
+  "close": "Đóng",
+  "transactionHistory": "Lịch sử giao dịch",
+  "enterCode": "Nhập mã đơn hàng",
+  "fromDate": "Từ ngày",
+  "toDate": "Đến ngày",
+  "statusStatus": "Trạng thái",
+  "successSuccess": "Thành công",
+  "pendingPayment": "Chờ thanh toán",
+  "transactionNo.": "Mã giao dịch / Mã đơn hàng",
+  "dateTime": "Ngày & giờ",
+  "amountAmount": "Số tiền",
+  "detailDetail": "Chi tiết",
+  "manageManage": "Quản lý",
+  "fullName": "Họ và tên",
+  "simNam": "SIM Việt Nam",
+  "validityPeriod": "Thời hạn sử dụng",
+  "daysDays": "ngày",
+  "qrCode": "Mã QR",
+  "getUs": "Liên hệ với chúng tôi",
+  "weArePossible": "Chúng tôi luôn sẵn sàng lắng nghe và hỗ trợ bạn với mọi thắc mắc. Vui lòng để lại thông tin, đội ngũ của chúng tôi sẽ liên hệ lại với bạn trong thời gian sớm nhất.",
+  "enterName": "Nhập họ và tên của bạn",
+  "specificRequirements": "Yêu cầu cụ thể",
+  "describeRequirements": "Mô tả yêu cầu của bạn",
+  "submitSubmit": "Gửi",
+  "from": "từ",
+  "noMatchesFound": "Không tìm thấy kết quả",
+  "viewAllDestinations": "Xem tất cả điểm đến",
+  "selectLanguage": "Chọn ngôn ngữ",
+  "weAreAlwaysHereToSupportYou": "Chúng tôi luôn ở đây để hỗ trợ bạn.",
+  "frequently": "Thường gặp",
+  "askedQuestions": "Câu hỏi được hỏi",
+  "fastestData": "Dữ liệu nhanh nhất",
+  "bestPrices": "Giá tốt nhất",
+  "connectInstantlyInOver200Destinations": "Kết nối ngay lập tức ở hơn 200 điểm đến",
+  "countryList": "Danh sách quốc gia",
+  "regionList": "Khu vực",
+  "noCountryMatchesFound": "Không tìm thấy quốc gia nào phù hợp với \"{searchQuery}\"",
+  "doesEsimWorkWithMyDevice": "Điện thoại của tôi có hỗ trợ eSIM không?",
+  "sayAboutUs": "nói về chúng tôi",
+  "overMillionSatisfiedCustomers": "Hơn 1.000.000 khách hàng hài lòng đã sử dụng dịch vụ của chúng tôi",
+  "simCountries": "Quốc gia SIM",
+  "apply": "Áp dụng",
+  "subtotal": "Tạm tính",
+  "checkout": "Thanh toán",
+  "getInTouch": "Liên hệ",
+  "withUs": "với chúng tôi",
+  "backToNews": "Quay lại Tin tức",
+  "myEsim": "eSIM của tôi",
+  "back": "Quay lại",
+  "failure": "Thất bại",
+  "guideAndHelp": "Hướng dẫn & Trợ giúp",
+  "selectTopicInstructions": "Chọn một chủ đề từ bên trái để xem hướng dẫn chi tiết cho SIM du lịch hoặc eSIM của bạn. Đội ngũ hỗ trợ của chúng tôi cũng sẵn sàng 24/7 qua trò chuyện.",
+  "importantNote": "Lưu ý quan trọng: Vui lòng đảm bảo thiết bị của bạn hỗ trợ eSIM trước khi mua hàng. Thông tin dưới đây chỉ mang tính tham khảo. Chúng tôi không chịu trách nhiệm về bất kỳ thay đổi nào trong thông tin thiết bị. Chúng tôi sẽ không hoàn tiền cho bất kỳ đơn hàng nào nếu thiết bị không hỗ trợ eSIM, bao gồm các trường hợp sau:",
+  "someDualPhysicalSimModelsDoNotSupportEsim": "Một số mẫu điện thoại có hai SIM vật lý không hỗ trợ eSIM.",
+  "carrierLockedDevicesMayNotSupportEsim": "Các thiết bị bị khóa nhà mạng (thiết bị đăng ký theo hợp đồng) có thể không hỗ trợ eSIM.",
+  "someModelsPurchasedInCertainCountriesMayNotSupportEsim": "Một số mẫu mua ở các quốc gia nhất định có thể không hỗ trợ eSIM (xem bảng bên dưới).",
+  "toMakeSureYourDeviceSupportsEsim": "Để chắc chắn thiết bị của bạn hỗ trợ eSIM, vui lòng liên hệ cửa hàng/nhà cung cấp nơi bạn đã mua thiết bị để có xác nhận chính xác nhất.",
+  "noteTheFollowingAppleDevicesDoNotSupportEsim": "*Lưu ý: Các thiết bị Apple sau KHÔNG hỗ trợ eSIM:",
+  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim": "Tất cả iPhone mua từ Trung Quốc, Hồng Kông và Ma Cao KHÔNG hỗ trợ eSIM.",
+  "method2CheckDirectlyOnYourDevice": "Phương pháp 2: Kiểm tra trực tiếp trên thiết bị của bạn",
+  "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing": "Kiểm tra bằng cách quay số *#06# và gọi, nếu hiển thị dòng EID (eSIM ID) là máy dùng được eSIM.",
+  "theStepsBelow": "các bước dưới đây",
+  "yourDeviceDoesNotSupportEsim": "Thiết bị của bạn không hỗ trợ eSIM?",
+  "tryFindingAPhysicalSimThatIsCompatibleWithYourDevice": "Hãy thử tìm một SIM vật lý tương thích với thiết bị của bạn",
+  "here": "tại đây",
+  "insightEngine": "Công cụ Thấu hiểu",
+  "realTimeWebSearchPoweredByGoogleGrounding": "Tìm kiếm web thời gian thực được hỗ trợ bởi Google Grounding",
+  "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ủ",
+  "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": "Kích hoạt tự động, hạ cánh là dùng (Plug and Play)",
+  "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 SkySimHub.",
+  "note2": "SkySimHub 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 nhà mạng lớn trong và ngoài nước",
+  "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 SkySimHub.",
+  "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",
+  "expiredAt": "Hết hạn vào",
+  "notActive": "Chưa kích hoạt",
+  "active": "Đang hoạt động",
+  "finished": "Đã sử dụng hết",
+  "expired": "Đã hết hạn",
+  "unknown": "Không xác định",
+  "paymentSuccess": "Thanh toán thành công",
+  "paymentFailed": "Thanh toán thất bại",
+  "activationMethod": "Phương thức kích hoạt",
+  "productDetailsMissing": "Vui lòng tải lại trang",
+  "goToShop": "Đi đến cửa hàng",
+  "english": "Tiếng Anh",
+  "vietnamese": "Tiếng Việt",
+  "numberOfDays": "Số ngày",
+  "verified": "Đã xác thực",
+  "highSpeed": "Tốc độ cao",
+  "selectADataPackageForYourDestination": "Sau khi xác nhận thiết bị, bạn có thể dễ dàng lựa chọn gói dữ liệu phù hợp với điểm đến và thời gian sử dụng. Các gói cước được thiết kế đa dạng về dung lượng, thời hạn và khu vực phủ sóng, đáp ứng mọi nhu cầu từ lướt web cơ bản, làm việc online đến sử dụng mạng xã hội và xem video.",
+  "checkIfYourPhoneSupportsESIMTechnology": "Trước khi mua, bạn chỉ cần kiểm tra xem điện thoại của mình có hỗ trợ eSIM. Hệ thống sẽ tự động xác định khả năng tương thích của thiết bị để đảm bảo bạn lựa chọn đúng sản phẩm phù hợp.  Cú pháp kiểm tra: *#06#",
+  "scanTheQRCodeAndConnectToHighSpeedData": "Sau khi hoàn tất thanh toán, eSIM sẽ được gửi trực tiếp về email hoặc tài khoản của bạn dưới dạng mã QR. Chỉ cần quét mã và thực hiện vài bước cài đặt đơn giản, eSIM sẽ được kích hoạt ngay lập tức. Đối với SIM vật lý, bạn chỉ cần lắp SIM vào thiết bị và bật dữ liệu di động để bắt đầu sử dụng. Không cần đăng ký phức tạp, không cần chờ đợi xác minh thủ công.",
+  "register": "Đăng ký",
+  "lao": "Tiếng Lào"
+}

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

@@ -0,0 +1,20 @@
+@import "tailwindcss";
+
+.absolute-right-18 {
+  right: -18px;
+}
+
+/* max 768px screen */
+@media (max-width: 768px) {
+  .absolute-right-18 {
+    right: 0px;
+  }
+}
+
+.font-black {
+  font-weight: 600 !important;
+}
+
+.color-EE0434 {
+  color: #EE0434;
+}

+ 24 - 0
EsimLao/esim-vite/src/index.tsx

@@ -0,0 +1,24 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
+import App from "./App";
+import { store } from "./app/store";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { Provider } from "react-redux";
+
+const rootElement = document.getElementById("root");
+if (!rootElement) {
+  throw new Error("Could not find root element to mount to");
+}
+
+const queryClient = new QueryClient();
+const root = ReactDOM.createRoot(rootElement);
+root.render(
+  <QueryClientProvider client={queryClient}>
+    <Provider store={store}>
+      <BrowserRouter>
+        <App />
+      </BrowserRouter>
+    </Provider>
+  </QueryClientProvider>
+);

+ 106 - 0
EsimLao/esim-vite/src/logic/loigicUtils.ts

@@ -0,0 +1,106 @@
+export const formatNumber = (val: number) => {
+  return new Intl.NumberFormat("vi-VN").format(val);
+};
+
+export const formatCarriers = (text) => {
+  if (!text) return "";
+  return text.replace(/,\s*/g, ", ");
+};
+
+export const setWithExpiry = <T>(
+  key: string,
+  value: T,
+  ttlMs?: number | null,
+) => {
+  const now = Date.now();
+
+  const item = {
+    value,
+    expiry: ttlMs ? now + ttlMs : now + 60 * 60 * 1000, // Default 1 hour
+  };
+
+  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;
+  }
+};
+
+type CurrencyCode =
+  | "VND"
+  | "USD"
+  | "EUR"
+  | "JPY"
+  | "KRW"
+  | "CNY"
+  | "SGD"
+  | "THB";
+
+export const formatCurrency = (
+  amount: number | string,
+  currency?: string,
+  locale?: string,
+) => {
+  if (amount === null || amount === undefined || amount === "") return "";
+
+  const value = Number(amount);
+
+  const resolvedLocale =
+    locale ??
+    (currency === "VND" ? "vi-VN" : currency === "USD" ? "en-US" : "en-GB");
+
+  return new Intl.NumberFormat(resolvedLocale, {
+    style: "currency",
+    currency,
+    minimumFractionDigits: currency === "VND" ? 0 : 2,
+  }).format(value);
+};
+
+export const convertOrderStatusToText = (status: number) => {
+  //   1: chờ thanh toán
+  // 2: đã thanh toán, chờ xuất esim
+  // 3: thanh toán thất bại
+  // 4: đã trả esim
+  switch (status) {
+    case 1:
+      return "Chờ thanh toán";
+    case 2:
+      return "Đã thanh toán, chờ xuất eSIM";
+    case 3:
+      return "Thanh toán thất bại";
+    case 4:
+      return "Đã trả eSIM";
+    default:
+      return "Không xác định";
+  }
+};
+
+export const convertOrderStatusToColor = (type: number) => {
+  switch (type) {
+    case 1:
+      return "bg-orange-400 text-white";
+    case 2:
+      return "bg-[#00c087] text-white";
+    case 3:
+      return "bg-red-400 text-white";
+    case 4:
+      return "bg-blue-400 text-white";
+    default:
+      return "bg-slate-800 text-white";
+  }
+};

+ 186 - 0
EsimLao/esim-vite/src/pages/buy-sim/BuySimView.tsx

@@ -0,0 +1,186 @@
+import React, { useEffect } from "react";
+import { ViewMode, SimProduct, SelectedProduct } from "../../services/types";
+import ProductCard from "../../components/ProductCard";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
+import { useAppDispatch } from "../../hooks/useRedux";
+import { productApi } from "../../apis/productApi";
+import { Area } from "../../services/product/type";
+import { DataCacheKey, staleTime } from "../../global/constants";
+import { useLocation, useNavigate } from "react-router-dom";
+import HomeSearch from "../home/components/HomeSearch";
+import { useTranslation } from "react-i18next";
+import { getWithExpiry } from "../../logic/loigicUtils";
+
+interface BuySimViewProps {
+  onProductSelect: (product: SelectedProduct) => void;
+  onViewChange: (view: ViewMode) => void;
+}
+
+const BuySimView: React.FC<BuySimViewProps> = ({
+  onProductSelect,
+  onViewChange,
+}) => {
+  const dispatch = useAppDispatch();
+  const navigate = useNavigate();
+  const { t } = useTranslation();
+  const location = useLocation();
+  const state = location.state;
+  const areas = getWithExpiry<Area[] | []>("areas");
+  // const [countries, setCountries] = React.useState<Area[]>([]);
+  // const [populars, setPopulars] = React.useState<Area[]>([]);
+
+  const { data: loadArea = [] } = useQuery<Area[]>({
+    queryKey: [DataCacheKey.AREAS],
+    queryFn: async (): Promise<Area[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await productApi.loadArea({
+          isCountry: "-1",
+          isPopular: "-1",
+        });
+        // save to redux store
+        console.log("Get area response data:", res);
+        let areas = res.data as Area[];
+        // setCountries(areas.filter((a) => a.isCountry === 1));
+        // setPopulars(areas.filter((a) => a.isPopular === 0));
+        return res.data as Area[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
+    },
+    staleTime: staleTime,
+  });
+
+  // const getAreaMutation = useMutation({
+  //   mutationFn: async () => {
+  //     dispatch(startLoading({}));
+  //     const res = await productApi.loadArea({
+  //       isCountry: "-1",
+  //       isPopular: "-1",
+  //     });
+  //     return res;
+  //   },
+  //   onSuccess: (data) => {
+  //     dispatch(stopLoading());
+  //     if (data && data.errorCode === "0") {
+  //       let areasGet = data.data as Area[];
+  //       setCountries(areasGet.filter((a) => a.isCountry === 1));
+  //       setPopulars(areasGet.filter((a) => a.isPopular === 0));
+  //     } else {
+  //       console.error("Get area failed, no token received");
+  //     }
+  //   },
+  //   onError: (error: any) => {
+  //     dispatch(stopLoading());
+  //     console.error("Get area error:", error.response.data);
+  //   },
+  // });
+
+  const handleSelect = (p: Area) => {
+    // open product detail view
+    navigate(`/product-detail/${p.id}`, {
+      state: {
+        ...p,
+      },
+    });
+  };
+
+  // useEffect(() => {
+  //   if (!areas || areas.length === 0) getAreaMutation.mutate();
+  //   else {
+  //     console.log("Areas loaded from cache:", areas.length, areas);
+  //     setCountries(areas.filter((a) => a.isCountry === 1));
+  //     setPopulars(areas.filter((a) => a.isPopular === 0));
+  //   }
+  // }, []);
+
+  useEffect(() => {
+    if (!state?.scrollToId || loadArea.length === 0) return;
+
+    requestAnimationFrame(() => {
+      requestAnimationFrame(() => {
+        scrollToSection();
+      });
+    });
+  }, [loadArea, state?.scrollToId]);
+
+  const scrollToSection = () => {
+    const element = document.getElementById(state?.scrollToId);
+    if (element) {
+      element.scrollIntoView({ behavior: "smooth", block: "start" });
+    }
+  };
+
+  const countries = React.useMemo(
+    () => loadArea.filter((a) => a.isCountry === 1),
+    [loadArea],
+  );
+
+  const regions = React.useMemo(
+    () => loadArea.filter((a) => a.isCountry === 0),
+    [loadArea],
+  );
+
+  return (
+    <div className="bg-[#fcfdfe] min-h-screen">
+      <div className="max-w-7xl mx-auto px-4 py-4 md:py-6 border-b border-slate-50">
+        <nav className="flex items-center space-x-2 text-xs md:text-sm text-slate-500 ">
+          <button
+            onClick={() => onViewChange(ViewMode.HOME)}
+            className="hover:text-[#EE0434] text-[18px]"
+          >
+            {t("home")}
+          </button>
+          <svg
+            className="w-3 h-3"
+            fill="none"
+            stroke="currentColor"
+            viewBox="0 0 24 24"
+          >
+            <path
+              d="M9 5l7 7-7 7"
+              strokeWidth={2.5}
+              strokeLinecap="round"
+              strokeLinejoin="round"
+            />
+          </svg>
+          <span className="text-slate-900 font-bold text-[18px]">
+            {t("buySim")}
+          </span>
+        </nav>
+      </div>
+
+      <div className="max-w-7xl mx-auto px-4">
+        <div className="mb-12">
+          <HomeSearch />
+        </div>
+        <section className="mb-12" id="countries">
+          <h2 className="text-xl md:text-[32px] font-black text-slate-900 mb-6 md:mb-8 tracking-tight">
+            {t("simCountries")}
+          </h2>
+          <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-6">
+            {countries.map((p) => (
+              <ProductCard key={p.id} p={p} onClick={handleSelect} />
+            ))}
+          </div>
+        </section>
+        <section className="mb-12" id="regions">
+          <h2 className="text-xl md:text-[32px] font-black text-slate-900 mb-6 md:mb-8 tracking-tight">
+            SIM {t("regionList")}
+          </h2>
+          <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-6">
+            {regions.map((p) => (
+              <ProductCard key={p.id} p={p} onClick={handleSelect} />
+            ))}
+          </div>
+        </section>
+      </div>
+    </div>
+  );
+};
+
+export default BuySimView;

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

@@ -0,0 +1,493 @@
+import React, { useState, useEffect } from "react";
+import { ViewMode } from "../../services/types";
+import { useLocation, useNavigate } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import {
+  Area,
+  CheckoutDetailResponse,
+  Package,
+  PaymentChannel,
+} from "../../services/product/type";
+import { DataCacheKey, staleTime } from "../../global/constants";
+import {
+  startLoading,
+  startSmallLoading,
+  stopLoading,
+  stopSmallLoading,
+} from "../../features/loading/loadingSlice";
+import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
+import { productApi } from "../../apis/productApi";
+import { openPopup } from "../../features/popup/popupSlice";
+import {
+  formatCurrency,
+  formatNumber,
+  getWithExpiry,
+} from "../../logic/loigicUtils";
+import { useTranslation } from "react-i18next";
+import { format } from "path";
+import { AccountInfo } from "../../services/auth/types";
+
+const CheckoutView = () => {
+  const navigate = useNavigate();
+  const dispatch = useAppDispatch();
+  const { t } = useTranslation();
+  const loading = useAppSelector((state) => state.loading);
+  const accountInfo = getWithExpiry("accountInfo") as AccountInfo | null;
+  const [paymentMethod, setPaymentMethod] = useState("card");
+  const [form, setForm] = useState({
+    firstName: accountInfo != null ? accountInfo.fullName?.split(" ")[0] : "",
+    lastName:
+      accountInfo != null
+        ? accountInfo.fullName?.split(" ").slice(1).join(" ")
+        : "",
+    email: accountInfo != null ? accountInfo.email : "",
+    confirmEmail: accountInfo != null ? accountInfo.email : "",
+    phone: accountInfo != null ? accountInfo.msisdn : "",
+  });
+  const [agreements, setAgreements] = useState({
+    terms: false,
+    esim: false,
+    vatInvoice: false,
+  });
+  // get data from previous step
+  const location = useLocation();
+  const state = location.state as {
+    area: Area;
+    package: Package;
+    quantity: number;
+    simType: "eSIM" | "Physical";
+    checkoutDetails: CheckoutDetailResponse;
+  };
+
+  if (state == null) {
+    // redirect to buy sim page
+    navigate("/buy-sim");
+    return null;
+  }
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const { name, value } = e.target;
+    setForm((prev) => ({ ...prev, [name]: value }));
+  };
+
+  const handleSubmit = async () => {
+    console.log(form);
+    if (form.email !== form.confirmEmail) {
+      dispatch(
+        openPopup({
+          isSuccess: false,
+          title: "Error",
+          message: "Email and Confirm Email do not match.",
+        }),
+      );
+      return;
+    }
+    // proceed to payment
+    dispatch(startSmallLoading());
+    const res = await productApi.confirmOrder({
+      surName: form.lastName,
+      lastName: form.firstName,
+      email: form.email,
+      phoneNumber: form.phone,
+      paymentChannelId: paymentMethod,
+      packgId: state.package.id,
+      quantity: state.quantity,
+    });
+    console.log("Confirm order response:", res);
+    dispatch(stopSmallLoading());
+    if (res.errorCode === "0") {
+      // open other website to pay
+      window.location.href = res.data.paymentUrl;
+    } else {
+      dispatch(
+        openPopup({
+          isSuccess: false,
+          title: "Error",
+          message: res.message || "Failed to confirm order.",
+        }),
+      );
+    }
+  };
+  const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const { name, checked } = e.target;
+    setAgreements((prev) => ({
+      ...prev,
+      [name]: checked,
+    }));
+  };
+  const { data: loadPaymentChannel = [] } = useQuery<PaymentChannel[]>({
+    queryKey: [DataCacheKey.PAYMENT_CHANNELS],
+    queryFn: async (): Promise<PaymentChannel[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await productApi.loadPaymentChannel();
+        // save to redux store
+        return res.data as PaymentChannel[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
+    },
+    staleTime: staleTime,
+  });
+
+  useEffect(() => {
+    setPaymentMethod(loadPaymentChannel[0]?.id);
+    dispatch(stopSmallLoading());
+  }, [loadPaymentChannel]);
+
+  // Styles matching LoginView inputs
+  const inputClass =
+    "w-full bg-slate-50 border-2 border-transparent focus:border-[#EE0434]/20 rounded-2xl py-3 md:py-4 px-5 focus:outline-none focus:bg-white transition-all text-slate-700 font-bold placeholder:text-slate-300";
+  const labelClass =
+    "block text-slate-400 font-black text-[14px] uppercase tracking-[0.2em] pl-1 mb-2";
+
+  if (!state.checkoutDetails) {
+    return null;
+  }
+  return (
+    <div className="bg-white min-h-screen pb-20">
+      {/* Header / Breadcrumb */}
+      <div className="max-w-7xl mx-auto px-4 py-4 md:py-6 border-b border-slate-50">
+        <div className="flex items-center space-x-2">
+          <button
+            onClick={() => navigate(-1)}
+            className="flex items-center text-slate-900 font-bold hover:text-[#EE0434] transition-colors text-[18px]"
+          >
+            <svg
+              className="w-5 h-5 mr-1"
+              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>
+            {t("continueShopping")}
+          </button>
+        </div>
+      </div>
+
+      <div className="max-w-3xl mx-auto px-4 py-8 md:py-12">
+        {/* Progress Steps */}
+        <div className="flex justify-center mb-12">
+          <div className="flex items-center w-full max-w-sm text-xs font-bold">
+            <div className="flex flex-col items-center gap-2">
+              <div className="w-8 h-8 rounded-full bg-[#00c087] text-white flex items-center justify-center">
+                <svg
+                  className="w-4 h-4"
+                  fill="none"
+                  stroke="currentColor"
+                  viewBox="0 0 24 24"
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={3}
+                    d="M5 13l4 4L19 7"
+                  />
+                </svg>
+              </div>
+              <span className="text-[#00c087] text-[14px]">
+                {t("chooseProduct")}
+              </span>
+            </div>
+            <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
+            <div className="flex flex-col items-center gap-2">
+              <div className="w-8 h-8 rounded-full bg-[#0071e3] text-white flex items-center justify-center">
+                2
+              </div>
+              <span className="text-[#0071e3] text-center text-[14px]">
+                {t("orderInformation")}
+              </span>
+            </div>
+            <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
+            <div className="flex flex-col items-center gap-2">
+              <div className="w-8 h-8 rounded-full bg-white border border-slate-200 text-slate-300 flex items-center justify-center">
+                3
+              </div>
+              <span className="text-slate-300 text-[14px]">
+                {t("paymentPayment")}
+              </span>
+            </div>
+          </div>
+        </div>
+
+        <div className="space-y-10">
+          {/* Customer Information */}
+          <div>
+            <h2 className="text-xl font-black text-[#003459] mb-6">
+              {t("customerInformation")}
+            </h2>
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+              <div className="space-y-1">
+                <label className={labelClass}>
+                  {t("lastName")} <span className="text-red-500">*</span>
+                </label>
+                <input
+                  type="text"
+                  className={inputClass}
+                  placeholder="Doe"
+                  onChange={handleChange}
+                  value={form.lastName}
+                  name="lastName"
+                />
+              </div>
+              <div className="space-y-1">
+                <label className={labelClass}>
+                  {t("firstName")} <span className="text-red-500">*</span>
+                </label>
+                <input
+                  type="text"
+                  className={inputClass}
+                  placeholder="John"
+                  onChange={handleChange}
+                  value={form.firstName}
+                  name="firstName"
+                />
+              </div>
+              <div className="space-y-1">
+                <label className={labelClass}>
+                  {t("emailEmail")} <span className="text-red-500">*</span>
+                </label>
+                <input
+                  type="email"
+                  className={inputClass}
+                  placeholder="john@example.com"
+                  onChange={handleChange}
+                  value={form.email}
+                  name="email"
+                />
+              </div>
+              <div className="space-y-1">
+                <label className={labelClass}>
+                  {t("confirmEmail")} <span className="text-red-500">*</span>
+                </label>
+                <input
+                  type="email"
+                  className={inputClass}
+                  placeholder="john@example.com"
+                  onChange={handleChange}
+                  value={form.confirmEmail}
+                  name="confirmEmail"
+                />
+              </div>
+              <div className="md:col-span-2 space-y-1">
+                <label className={labelClass}>{t("phoneNumber")}</label>
+                <input
+                  type="tel"
+                  className={inputClass}
+                  placeholder="+1 234 567 890"
+                  onChange={handleChange}
+                  value={form.phone}
+                  name="phone"
+                />
+              </div>
+            </div>
+          </div>
+
+          {/* Order Info */}
+          <div className="bg-slate-50/50 rounded-3xl p-6 border border-slate-100">
+            <h2 className="text-xl font-black text-[#003459] mb-6">
+              {t("orderInformation")}
+            </h2>
+            <div className="flex justify-between items-center">
+              <div className="flex items-center gap-4">
+                <img
+                  src={`${state.area.iconUrl}`}
+                  className="w-10 h-10 rounded-full object-cover border-2 border-white shadow-sm"
+                />
+                <div>
+                  <h3 className="font-black text-slate-800 text-lg">
+                    eSIM {state.area.areaName1}
+                  </h3>
+                  <p className="text-xs text-slate-500 font-bold uppercase tracking-wide mt-1">
+                    {/* {state.package.amountData} -  */}
+                    {state.package.dayDuration} days • x{state.quantity}
+                  </p>
+                </div>
+              </div>
+              <div className="text-right">
+                <p className="text-xs text-slate-400 line-through font-bold">
+                  {formatCurrency(
+                    state.checkoutDetails.totalMoney,
+                    state.checkoutDetails.curency,
+                  )}
+                </p>
+                <p className="text-[#0071e3] font-black text-xl">
+                  {formatCurrency(
+                    state.checkoutDetails.paymentMoney,
+                    state.checkoutDetails.curency,
+                  )}
+                </p>
+              </div>
+            </div>
+          </div>
+
+          {/* Payment Method */}
+          <div>
+            <h2 className="text-xl font-black text-[#003459] mb-6">
+              {t("paymentPayment")}
+            </h2>
+            <div className="space-y-3">
+              {loadPaymentChannel.map((method) => (
+                <label
+                  key={method.id}
+                  className={`flex items-center p-4 border rounded-2xl cursor-pointer transition-all ${
+                    paymentMethod === method.id
+                      ? "bg-blue-50 border-blue-200"
+                      : "border-slate-100 hover:bg-slate-50"
+                  }`}
+                >
+                  <input
+                    type="radio"
+                    name="payment"
+                    checked={paymentMethod === method.id}
+                    onChange={() => setPaymentMethod(method.id)}
+                    className="w-5 h-5 text-[#EE0434] border-slate-300 focus:ring-[#EE0434]"
+                  />
+                  <span className="ml-4 font-bold text-slate-700 flex items-center gap-3">
+                    <span className="text-xl w-6 text-center">
+                      {/* {method.imgUrl} */}
+                    </span>{" "}
+                    {method.name}
+                  </span>
+                </label>
+              ))}
+            </div>
+          </div>
+
+          {/* Terms */}
+          <div className="space-y-3 pt-2 px-1">
+            <label className="flex items-start cursor-pointer group">
+              <input
+                type="checkbox"
+                name="terms"
+                checked={agreements.terms}
+                onChange={handleCheckboxChange}
+                className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
+              />
+              <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
+                {t("iService")}{" "}
+                <a
+                  href="#"
+                  className="text-[#0071e3] font-bold hover:underline"
+                >
+                  {t("termsService")}
+                </a>
+              </span>
+            </label>
+            <label className="flex items-start cursor-pointer group">
+              <input
+                type="checkbox"
+                className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
+                name="esim"
+                checked={agreements.esim}
+                onChange={handleCheckboxChange}
+              />
+              <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
+                {t("iUnlocked")}
+              </span>
+            </label>
+            {/* <label className="flex items-start cursor-pointer group">
+              <input
+                type="checkbox"
+                className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
+                name="vatInvoice"
+                checked={agreements.vatInvoice}
+                onChange={handleCheckboxChange}
+              />
+              <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
+                {t("iInvoice")}
+              </span>
+            </label> */}
+          </div>
+
+          {/* Overview Card */}
+          <div className="bg-[#eef6f8] rounded-[32px] p-8 mt-8">
+            <h2 className="text-xl font-black text-[#003459] mb-6">Overview</h2>
+
+            <div className="relative mb-8">
+              <input
+                type="text"
+                placeholder="Promo code"
+                className="w-full bg-white border-2 border-transparent focus:border-[#0071e3]/20 rounded-2xl py-4 px-6 focus:outline-none transition-all text-slate-700 font-bold placeholder:text-slate-300"
+              />
+              <button className="absolute right-3 top-2 bottom-2 text-[#0071e3] font-black text-sm px-4 hover:bg-blue-50 rounded-xl transition-colors">
+                {t("apply")}
+              </button>
+            </div>
+
+            <div className="space-y-4 mb-8">
+              <div className="flex justify-between text-slate-600 font-bold">
+                <span>{t("subtotal")}:</span>
+                <span>
+                  {formatCurrency(
+                    state.checkoutDetails.totalMoney * state.quantity,
+                    state.checkoutDetails.curency,
+                  )}
+                </span>
+              </div>
+              <div className="flex justify-between text-green-600 font-bold">
+                <span>Discount:</span>
+                <span>
+                  -
+                  {formatCurrency(
+                    state.checkoutDetails.discount,
+                    state.checkoutDetails.curency,
+                  )}
+                </span>
+              </div>
+              <div className="flex justify-between items-center pt-4 border-t border-slate-200/50">
+                <span className="text-[#0071e3] font-black text-xl">
+                  {t("totalTotal")} ({state.quantity}):
+                </span>
+                <span className="text-[#0071e3] font-black text-3xl">
+                  {formatCurrency(
+                    state.checkoutDetails.paymentMoney,
+                    state.checkoutDetails.curency,
+                  )}
+                </span>
+              </div>
+            </div>
+
+            <button
+              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 ${
+                form.firstName &&
+                form.lastName &&
+                form.email &&
+                agreements.terms &&
+                agreements.esim &&
+                !loading.isSmallLoading
+                  ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
+                  : "bg-slate-100 text-slate-300 cursor-not-allowed"
+              }`}
+              disabled={
+                !form.firstName ||
+                !form.lastName ||
+                !form.email ||
+                !agreements.terms ||
+                !agreements.esim ||
+                loading.isSmallLoading
+              }
+              onClick={handleSubmit}
+            >
+              {loading.isSmallLoading && (
+                <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
+              )}
+              <span>{t("checkout")}</span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default CheckoutView;

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

@@ -0,0 +1,80 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+
+const ContactView: React.FC = () => {
+  const { t } = useTranslation();
+  return (
+    <div className="bg-white">
+      <section className="max-w-7xl mx-auto px-6 md:px-12 py-16 md:py-24">
+        <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
+          <div className="space-y-8">
+            <h1 className="text-5xl md:text-7xl font-black text-[#EE0434] tracking-tight leading-tight">
+              {t("getInTouch")} <br /> {t("withUs")}
+            </h1>
+            <p className="text-slate-500 text-lg md:text-xl font-medium leading-relaxed max-w-lg">
+              {t("weAreAlwaysHereToSupportYou")}
+            </p>
+            <div className="space-y-6 pt-4">
+              <div className="space-y-2">
+                <div className="flex items-center space-x-3 text-slate-800">
+                  <svg
+                    className="w-6 h-6 text-[#EE0434]"
+                    fill="none"
+                    stroke="currentColor"
+                    viewBox="0 0 24 24"
+                  >
+                    <path
+                      d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
+                      strokeWidth={2}
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
+                    />
+                  </svg>
+                  <span className="font-bold text-lg">Email</span>
+                </div>
+                <a
+                  href="mailto:klinhnguyen@viettech.asia"
+                  className="block text-[#EE0434] font-black text-lg md:text-xl pl-9 hover:underline"
+                >
+                  klinhnguyen@viettech.asia
+                </a>
+              </div>
+            </div>
+          </div>
+          <div className="bg-white p-2">
+            <form className="space-y-8" onSubmit={(e) => e.preventDefault()}>
+              <div className="space-y-3">
+                <label className="block text-slate-800 font-bold text-lg">
+                  {t("fullName")}
+                </label>
+                <input
+                  type="text"
+                  placeholder={t("enterName")}
+                  className="w-full bg-white border border-slate-200 rounded-xl py-4 px-6 focus:outline-none focus:ring-2 focus:ring-red-50 focus:border-[#EE0434] transition-all"
+                />
+              </div>
+              <div className="space-y-3">
+                <label className="block text-slate-800 font-bold text-lg">
+                  {t("specificRequirements")}
+                </label>
+                <textarea
+                  rows={6}
+                  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>
+              <button
+                type="submit"
+                className="w-full bg-[#EE0434] text-white py-5 rounded-[40px] font-black text-2xl shadow-lg hover:shadow-xl transition-all"
+              >
+                {t("submitSubmit")}
+              </button>
+            </form>
+          </div>
+        </div>
+      </section>
+    </div>
+  );
+};
+
+export default ContactView;

+ 484 - 0
EsimLao/esim-vite/src/pages/home/HomeView.tsx

@@ -0,0 +1,484 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { SimProduct } from "../../services/types";
+import HomeBanner from "./components/HomeBanner";
+import HomeTestimonial from "./components/HomeTestimonial";
+import HomeProduct from "./components/HomeProduct";
+import HomeSearch from "./components/HomeSearch";
+import HomeFaq from "./components/HomeFaq";
+import { useAppDispatch } from "../../hooks/useRedux";
+import { openPopup } from "../../features/popup/popupSlice";
+import partner1 from "../../assets/img/partner1.png";
+import partner2 from "../../assets/img/partner2.png";
+import { authApi } from "../../apis/authApi";
+import { accountLogin } from "../../features/account/accuntSlice";
+import { useTranslation } from "react-i18next";
+import i18n from "../../i18n";
+import CryptoJS from "crypto-js";
+import { getWithExpiry, setWithExpiry } from "../../logic/loigicUtils";
+
+const HomeView: React.FC = () => {
+  const [simType, setSimType] = useState<"eSIM" | "Physical">("eSIM");
+  const [searchParams] = useSearchParams();
+  const navigate = useNavigate();
+  const dispatch = useAppDispatch();
+  const { t } = useTranslation();
+  const langNow = (getWithExpiry("selectedLanguage") as string) || "en";
+
+  useEffect(() => {
+    // set language in i18n
+    i18n.changeLanguage(langNow);
+    setWithExpiry("selectedLanguage", langNow);
+  }, [langNow]);
+
+  useEffect(() => {
+    const params = new URLSearchParams(window.location.search);
+
+    console.log("URL Params: ", params.toString());
+    const status = params.get("vpc_TxnResponseCode");
+
+    console.log("searchParams: ", searchParams.toString());
+
+    // google callback
+    const code = searchParams.get("code");
+    const partner = searchParams.get("partner");
+    const data = searchParams.get("data");
+    const lang = searchParams.get("lang");
+
+    if (lang) {
+      i18n.changeLanguage(lang);
+      setWithExpiry("selectedLanguage", lang);
+    }
+
+    if (status) {
+      console.log("URL Params:", params);
+      if (status === "0") {
+        dispatch(
+          openPopup({
+            isSuccess: true,
+            message: t("paymentSuccess"),
+            title: t("paymentStatus"),
+            buttonText: t("close"),
+            hasRightButton: true,
+            rightButtonText: t("viewOrder"),
+            rightButtonAction: "OPEN_ORDER_HISTORY",
+          }),
+        );
+      } else {
+        dispatch(
+          openPopup({
+            isSuccess: false,
+            message: t("paymentFailed"),
+            title: t("paymentStatus"),
+            buttonText: t("close"),
+          }),
+        );
+      }
+    } else if (code) {
+      console.log("Handling Google callback with code:", code);
+      handleGoogleCallback(code);
+    } else if (partner && data) {
+      console.log("Handling partner callback with partner:", partner);
+      // You can implement partner callback handling here
+      // aes decryption can be done in the backend for security reasons but for test, we do it here
+      setWithExpiry("selectedPartner", partner);
+      handlePartnerCallback(partner, data);
+    }
+  }, []);
+
+  const handlePartnerCallback = async (partner: string, data: string) => {
+    try {
+      const response = await authApi.partnerLogin({
+        partner,
+        data: data,
+      });
+      if (response.errorCode === "0") {
+        dispatch(accountLogin(response.data));
+        navigate("/");
+      } else {
+        console.error("Google callback failed:", response.message);
+      }
+    } catch (error) {
+      console.error("Partner callback error:", error);
+    }
+  };
+
+  const handleGoogleCallback = async (code: string) => {
+    try {
+      const response = await authApi.googleCallback({ code });
+      if (response.errorCode === "0") {
+        dispatch(accountLogin(response.data));
+        navigate("/");
+      } else {
+        console.error("Google callback failed:", response.message);
+      }
+    } catch (error) {
+      console.error("Google callback error:", error);
+    }
+  };
+
+  const steps = [
+    {
+      number: "1",
+      title: t("compatibleDevice"),
+      description: t("checkIfYourPhoneSupportsESIMTechnology"),
+    },
+    {
+      number: "2",
+      title: t("pickPlan"),
+      description: t("selectADataPackageForYourDestination"),
+    },
+    {
+      number: "3",
+      title: t("instantActivation"),
+      description: t("scanTheQRCodeAndConnectToHighSpeedData"),
+    },
+  ];
+
+  return (
+    <div className="bg-white overflow-x-hidden">
+      <style>{`
+        @keyframes marquee-up { 0% { transform: translateY(0); } 100% { transform: translateY(-50%); } }
+        @keyframes marquee-down { 0% { transform: translateY(-50%); } 100% { transform: translateY(0); } }
+        @keyframes scroll-x { 0% { transform: translateX(0); } 100% { transform: translateX(-33.33%); } }
+        @keyframes progress-bar { 0% { width: 0%; } 100% { width: 100%; } }
+        .animate-marquee-up { animation: marquee-up 40s linear infinite; }
+        .animate-marquee-down { animation: marquee-down 40s linear infinite; }
+        .animate-scroll-x { animation: scroll-x 10s linear infinite; }
+        .animate-progress { animation: progress-bar 6s linear infinite; }
+      `}</style>
+
+      {/* Main Search Section */}
+      <HomeSearch />
+
+      {/* Hero Banner */}
+      <HomeBanner />
+
+      {/* Product Selection */}
+      <HomeProduct />
+
+      {/* Customer Testimonials */}
+      <HomeTestimonial />
+
+      {/* WHY CHOOSE INFIGATE */}
+      <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">
+            <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>
+            <div className="absolute bottom-0 left-0 w-[300px] h-[300px] bg-black/10 rounded-full blur-[80px] translate-y-1/2 -translate-x-1/2"></div>
+
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-x-20 md:gap-y-16 relative z-10">
+              <div className="space-y-6">
+                <div className="flex items-center space-x-4">
+                  <div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/20 flex items-center justify-center shrink-0 border border-white/10 shadow-lg backdrop-blur-sm">
+                    <svg
+                      className="w-6 h-6 md:w-8 md:h-8 text-white"
+                      fill="none"
+                      stroke="currentColor"
+                      viewBox="0 0 24 24"
+                    >
+                      <path
+                        strokeLinecap="round"
+                        strokeLinejoin="round"
+                        strokeWidth={2}
+                        d="M13 10V3L4 14h7v7l9-11h-7z"
+                      />
+                    </svg>
+                  </div>
+                  <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
+                    {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>{t("internationalTravelStarts")}</li>
+                  <li>{t("easilyChooseSuitableSim")}</li>
+                  <li>{t("eSIMReceiveQR")}</li>
+                  <li>{t("physicalSIMDelivery")}</li>
+                </ul>
+              </div>
+
+              <div className="space-y-6">
+                <div className="flex items-center space-x-4">
+                  <div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/20 flex items-center justify-center shrink-0 border border-white/10 shadow-lg backdrop-blur-sm">
+                    <svg
+                      className="w-6 h-6 md:w-8 md:h-8 text-white"
+                      fill="none"
+                      stroke="currentColor"
+                      viewBox="0 0 24 24"
+                    >
+                      <path
+                        strokeLinecap="round"
+                        strokeLinejoin="round"
+                        strokeWidth={2}
+                        d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
+                      />
+                    </svg>
+                  </div>
+                  <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
+                    {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>{t("fastestData")}</li>
+                  <li>{t("worldLeadingNetwork")}</li>
+                  <li>{t("flexibleDiversePackages")}</li>
+                </ul>
+              </div>
+
+              <div className="space-y-6">
+                <div className="flex items-center space-x-4">
+                  <div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/20 flex items-center justify-center shrink-0 border border-white/10 shadow-lg backdrop-blur-sm">
+                    <svg
+                      className="w-6 h-6 md:w-8 md:h-8 text-white"
+                      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>
+                  </div>
+                  <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
+                    {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>{t("customerServiceAvailable")}</li>
+                  <li>{t("supportChannels")}</li>
+                </ul>
+              </div>
+
+              <div className="space-y-6">
+                <div className="flex items-center space-x-4">
+                  <div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/20 flex items-center justify-center shrink-0 border border-white/10 shadow-lg backdrop-blur-sm">
+                    <svg
+                      className="w-6 h-6 md:w-8 md:h-8 text-white"
+                      fill="none"
+                      stroke="currentColor"
+                      viewBox="0 0 24 24"
+                    >
+                      <path
+                        strokeLinecap="round"
+                        strokeLinejoin="round"
+                        strokeWidth={2}
+                        d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
+                      />
+                    </svg>
+                  </div>
+                  <h3 className="text-lg md:text-2xl font-black text-white leading-tight">
+                    {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>{t("refundDefective")}</li>
+                  <li>{t("commitmentPeaceOfMind")}</li>
+                </ul>
+              </div>
+            </div>
+          </div>
+        </div>
+      </section>
+
+      {/* PARTNERSHIP */}
+      <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">
+            SkySimHub <span className="text-[#EE0434]">{t("getgoWith")}</span>
+          </h2>
+
+          <div className="space-y-8 md:space-y-12 max-w-5xl mx-auto">
+            <div className="space-y-4 md:space-y-6">
+              <div className="flex items-center space-x-3">
+                <div className="w-5 h-5 md:w-6 md:h-6 bg-[#EE0434] rounded-full flex items-center justify-center shrink-0">
+                  <svg
+                    className="w-3 h-3 md:w-4 md:h-4 text-white"
+                    fill="currentColor"
+                    viewBox="0 0 20 20"
+                  >
+                    <path
+                      fillRule="evenodd"
+                      d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
+                      clipRule="evenodd"
+                    />
+                  </svg>
+                </div>
+                <p className="text-sm md:text-2xl font-semibold text-slate-500">
+                  {t("partnershipNetwork")}
+                </p>
+              </div>
+              <div className="flex flex-wrap gap-3 md:gap-8 justify-center lg:justify-start">
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/1.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/2.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/3.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/4.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/5.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/6.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/7.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+                <div className="bg-white border border-[#EE0434]/20 rounded-xl px-4 py-2 w-36 md:w-56 h-14 flex items-center justify-center shadow-sm">
+                  <img
+                    src="https://skysimhub.vn/assets/partners/8.png"
+                    className="max-h-full max-w-full object-contain"
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </section>
+
+      {/* How it Works */}
+      {/* <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">
+            {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">
+              <button
+                onClick={() => setSimType("eSIM")}
+                className={`px-6 md:px-12 py-2.5 md:py-3 rounded-full text-lg md:text-xs lg:text-xl font-bold transition-all ${
+                  simType === "eSIM"
+                    ? "bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white shadow-md"
+                    : "bg-[#f0f0f0] text-[#8b8e96] hover:text-slate-800"
+                }`}
+              >
+                eSIM / {t("physicalSim")}
+              </button>
+              <button
+                onClick={() => setSimType("Physical")}
+                className={`px-6 md:px-12 py-2.5 md:py-3 rounded-full text-lg md:text-xs lg:text-xl font-bold transition-all ${
+                  simType === "Physical"
+                    ? "bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white shadow-md"
+                    : "bg-[#f0f0f0] text-[#8b8e96] hover:text-slate-800"
+                }`}
+              >
+                {t("physicalSim")}
+              </button>
+            </div>
+          </div>
+          <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 md:gap-16 items-center">
+            <div className="relative aspect-square bg-[#EE0434]/5 rounded-[24px] md:rounded-[48px] overflow-hidden flex items-center justify-center shadow-xl border border-red-50">
+              <div className="absolute inset-0 bg-gradient-to-br from-[#EE0434]/20 to-white/0 opacity-50"></div>
+              <div className="w-4/5 h-4/5 relative flex items-center justify-center">
+                <svg
+                  className="w-full h-full text-[#EE0434] drop-shadow-lg"
+                  viewBox="0 0 100 100"
+                  fill="none"
+                >
+                  <path
+                    d="M0 70 Q 25 20, 50 70 T 100 70"
+                    stroke="currentColor"
+                    strokeWidth="8"
+                    strokeLinecap="round"
+                    className="opacity-80"
+                  />
+                </svg>
+              </div>
+            </div>
+            <div className="space-y-4 md:space-y-12">
+              {steps.map((step, index) => (
+                <div
+                  key={index}
+                  className="flex items-start space-x-4 md:space-x-6 p-4 md:p-8 rounded-[20px] md:rounded-[32px] border bg-transparent border-transparent hover:bg-white hover:shadow-lg hover:border-slate-50 transition-all"
+                >
+                  <span className="text-xl md:text-3xl font-black text-[#EE0434] mt-0.5 shrink-0">
+                    {step.number}
+                  </span>
+                  <div>
+                    <h3 className="text-base md:text-2xl font-black text-slate-900 mb-0.5 md:mb-2">
+                      {step.title}
+                    </h3>
+                    <p className="text-slate-500 text-[11px] md:text-lg leading-relaxed">
+                      {step.description}
+                    </p>
+                  </div>
+                </div>
+              ))}
+            </div>
+          </div>
+        </div>
+      </section> */}
+
+      {/* FAQ */}
+      <HomeFaq />
+
+      {/* Refund */}
+      <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">
+            {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"
+            onClick={() => {
+              navigate("/terms");
+            }}
+          >
+            <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center text-slate-800">
+              <svg
+                className="w-8 h-8"
+                fill="none"
+                stroke="currentColor"
+                viewBox="0 0 24 24"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2.5}
+                  d="M17 8l4 4m0 0l-4 4m4-4H3"
+                />
+              </svg>
+            </div>
+            <span className="ml-6 text-white text-2xl font-black">
+              {t("returnGuide")}
+            </span>
+          </button>
+        </div>
+      </section>
+    </div>
+  );
+};
+
+export default HomeView;

+ 115 - 0
EsimLao/esim-vite/src/pages/home/components/HomeBanner.tsx

@@ -0,0 +1,115 @@
+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 { 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) % loadBannerData.length);
+  }, [loadBannerData.length]);
+
+  useEffect(() => {
+    const timer = setInterval(nextBanner, 6000);
+    return () => clearInterval(timer);
+  }, [nextBanner]);
+
+  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">
+        {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 ${
+              idx === currentBanner
+                ? "opacity-100 z-20 scale-100"
+                : "opacity-0 z-10 pointer-events-none scale-105"
+            }`}
+          >
+            <div
+              className={`absolute inset-0 bg-gradient-to-br ${
+                gradients[idx % gradients.length]
+              } opacity-90 transition-all`}
+              style={{ backgroundImage: `url(${banner.imageUrl})` }}
+            ></div>
+
+            <div className="relative z-30 flex flex-col items-center justify-center p-6 md:p-24 w-full h-full text-center">
+              <div
+                className={`text-white space-y-6 md:space-y-8 transition-all duration-1000 delay-300 ${
+                  idx === currentBanner
+                    ? "translate-y-0 opacity-100"
+                    : "translate-y-12 opacity-0"
+                }`}
+              >
+                <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.title}
+                </div>
+                <h1 className="text-4xl md:text-4xl lg:text-5xl font-black leading-[1.1] tracking-tighter max-w-4xl">
+                  {banner.title} <br />
+                  <span className="text-white/60 drop-shadow-sm">
+                    {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.title}
+                </p>
+              </div>
+            </div>
+          </div>
+        ))}
+
+        <div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-40 flex items-center space-x-4">
+          {loadBannerData.map((_, i) => (
+            <button
+              key={i}
+              onClick={() => setCurrentBanner(i)}
+              className={`group relative h-1.5 transition-all duration-500 overflow-hidden rounded-full ${
+                i === currentBanner ? "w-16 bg-white/20" : "w-8 bg-white/40"
+              }`}
+            >
+              {i === currentBanner && (
+                <div className="absolute inset-0 bg-white animate-progress"></div>
+              )}
+            </button>
+          ))}
+        </div>
+      </div>
+    </section>
+  );
+};
+
+export default HomeBanner;

+ 130 - 0
EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx

@@ -0,0 +1,130 @@
+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";
+import { useTranslation } from "react-i18next";
+import { getWithExpiry } from "../../../logic/loigicUtils";
+
+const HomeFaq = () => {
+  const [openFaqIndex, setOpenFaqIndex] = useState<number | null>(0);
+  const dispatch = useAppDispatch();
+  const { t } = useTranslation();
+  const partner = getWithExpiry("selectedPartner") as string | null;
+  const { data: loadFaqsData = [] } = useQuery<Faq[]>({
+    queryKey: [DataCacheKey.FAQS],
+    queryFn: async (): Promise<Faq[]> => {
+      try {
+        dispatch(startLoading({}));
+        const res = await contentApi.LoadFaq({
+          pageNumber: 0,
+          pageSize: 10,
+          isFeatured: true,
+        });
+        return res.data.faqs as Faq[];
+      } catch (error) {
+        console.error(error);
+        return []; // 🔴 bắt buộc
+      } finally {
+        dispatch(stopLoading());
+      }
+    },
+    staleTime: staleTime,
+  });
+
+  useEffect(() => {
+    window.scrollTo({ top: 0, behavior: "smooth" });
+    console.log("parter: ", partner);
+  }, []);
+
+  const toggleFaq = (index: number) => {
+    setOpenFaqIndex(openFaqIndex === index ? null : index);
+  };
+
+  return (
+    partner !== "laotravel" && (
+      <>
+        <section className="py-16 md:py-24 bg-white px-4 border-t border-slate-50">
+          <div className="max-w-4xl mx-auto">
+            <div className="text-center mb-12 md:mb-20">
+              <h2 className="text-3xl md:text-6xl font-black tracking-tight text-[#EE0434] mb-4">
+                {t("frequently")}{" "}
+                <span className="text-slate-900">{t("askedQuestions")}</span>
+              </h2>
+              <p className="text-slate-500 text-sm md:text-xl font-medium">
+                {t("weAreAlwaysHereToSupportYou")}
+              </p>
+            </div>
+
+            <div className="space-y-4">
+              {loadFaqsData.map((faq, index) => (
+                <div key={index} className="border-b border-slate-100">
+                  <button
+                    onClick={() => toggleFaq(index)}
+                    className="w-full flex justify-between items-center py-6 text-left group transition-all"
+                  >
+                    <span
+                      className={`text-base md:text-2xl font-black transition-colors ${
+                        openFaqIndex === index
+                          ? "text-[#EE0434]"
+                          : "text-slate-800 group-hover:text-[#EE0434]"
+                      }`}
+                    >
+                      {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 ${
+                        openFaqIndex === index
+                          ? "text-[#EE0434] rotate-180"
+                          : "text-slate-300"
+                      }`}
+                    >
+                      <svg
+                        className="w-5 h-5 md:w-7 md:h-7"
+                        fill="none"
+                        stroke="currentColor"
+                        viewBox="0 0 24 24"
+                      >
+                        <path
+                          strokeLinecap="round"
+                          strokeLinejoin="round"
+                          strokeWidth={3}
+                          d="M19 9l-7 7-7-7"
+                        />
+                      </svg>
+                    </div>
+                  </button>
+                  <div
+                    className={`grid transition-all duration-500 ease-in-out ${
+                      openFaqIndex === index
+                        ? "grid-rows-[1fr] opacity-100"
+                        : "grid-rows-[0fr] opacity-0"
+                    }`}
+                  >
+                    <div className="overflow-hidden">
+                      {/* show html */}
+                      <div
+                        dangerouslySetInnerHTML={{ __html: faq.answer }}
+                        className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8"
+                      />
+                      {/* <p className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8">
+                      {faq.answer}
+                    </p> */}
+                    </div>
+                  </div>
+                </div>
+              ))}
+            </div>
+          </div>
+        </section>
+      </>
+    )
+  );
+};
+
+export default HomeFaq;

+ 150 - 0
EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx

@@ -0,0 +1,150 @@
+import { useAppDispatch, useAppSelector } from "../../../hooks/useRedux";
+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";
+import {
+  startLoading,
+  stopLoading,
+} from "../../../features/loading/loadingSlice";
+import { DataCacheKey, staleTime } from "../../../global/constants";
+import { Area } from "../../../services/product/type";
+import { formatCurrency, formatNumber } from "../../../logic/loigicUtils";
+import { useTranslation } from "react-i18next";
+import ProductCard from "../../../components/ProductCard";
+
+const HomeProduct = () => {
+  const [activeTab, setActiveTab] = useState<"country" | "region">("country");
+  const navigate = useNavigate();
+  const [areasList, setAreasList] = useState<Area[]>([]);
+  const { t } = useTranslation();
+  const dispatch = useAppDispatch();
+
+  useEffect(() => {
+    getAreaMutation.mutate();
+  }, []);
+
+  const getAreaMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await productApi.loadArea(
+        activeTab === "country"
+          ? {
+              isCountry: "1",
+              isPopular: "1",
+            }
+          : {
+              isCountry: "0",
+              isPopular: "1",
+            },
+      );
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopLoading());
+      console.log("Get area response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Get area successful");
+        const areas = data.data as Area[];
+        setAreasList(areas.slice(0, 8));
+      } else {
+        console.error("Get area failed, no token received");
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopLoading());
+      console.error("Get area error:", error.response.data);
+    },
+  });
+
+  const handleProductClick = (p: Area) => {
+    navigate(`/product-detail/${p.id}`, {
+      state: {
+        ...p,
+      },
+    });
+    window.scrollTo({ top: 0, behavior: "smooth" });
+  };
+
+  return (
+    <section className="pt-12 pb-20 text-center px-4">
+      <h2 className="text-3xl md:text-5xl lg:text-6xl font-black text-slate-900 tracking-tight mb-4">
+        {t("fastestData")},{" "}
+        <span className="text-[#EE0434]">{t("bestPrices")}</span>
+      </h2>
+      <p className="text-slate-500 font-bold mb-10 md:mb-12">
+        {t("connectInstantlyInOver200Destinations")}
+      </p>
+
+      <div className="inline-flex p-1.5 bg-slate-100 rounded-full mb-12 md:mb-16 shadow-inner">
+        <button
+          onClick={() => {
+            setActiveTab("country");
+            getAreaMutation.mutate();
+          }}
+          className={`px-8 md:px-12 py-3 rounded-full text-lg md:text-xs lg:text-xl font-black transition-all ${
+            activeTab === "country"
+              ? "bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white shadow-lg"
+              : "bg-[#f0f0f0] text-[#8b8e96] hover:text-slate-800"
+          }`}
+        >
+          {t("countryList")}
+        </button>
+        <button
+          onClick={() => {
+            setActiveTab("region");
+            getAreaMutation.mutate();
+          }}
+          className={`px-8 md:px-12 py-3 rounded-full text-lg md:text-xs lg:text-xl  font-black transition-all ${
+            activeTab === "region"
+              ? "bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white shadow-lg"
+              : "bg-[#f0f0f0] text-[#8b8e96] hover:text-slate-800"
+          }`}
+        >
+          {t("regionList")}
+        </button>
+      </div>
+
+      <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">
+          {areasList.map((p) => (
+            <ProductCard key={p.id} p={p} onClick={handleProductClick} />
+          ))}
+        </div>
+      </div>
+      <div className="max-w-7xl mx-auto flex flex-col items-center mt-16">
+        <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"
+          onClick={() =>
+            navigate("/buy-sim", {
+              state: {
+                scrollToId: activeTab === "country" ? "countries" : "regions",
+              },
+            })
+          }
+        >
+          <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center text-slate-800">
+            <svg
+              className="w-8 h-8"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                strokeLinecap="round"
+                strokeLinejoin="round"
+                strokeWidth={2.5}
+                d="M17 8l4 4m0 0l-4 4m4-4H3"
+              />
+            </svg>
+          </div>
+          <span className="ml-6 text-white text-lg font-black">
+            {t("seeMore")}
+          </span>
+        </button>
+      </div>
+    </section>
+  );
+};
+
+export default HomeProduct;

+ 217 - 0
EsimLao/esim-vite/src/pages/home/components/HomeSearch.tsx

@@ -0,0 +1,217 @@
+import { openCompatibilityModal } from "../../../features/popup/popupSlice";
+import { useDispatch, useSelector } from "react-redux";
+import { useState, useRef, useEffect } from "react";
+import { useMutation } from "@tanstack/react-query";
+import {
+  startLoading,
+  stopLoading,
+} from "../../../features/loading/loadingSlice";
+import { productApi } from "../../../apis/productApi";
+import { setAreas } from "../../../features/areas/areasSlice";
+import { Area } from "../../../services/product/type";
+import { useNavigate } from "react-router";
+import { useTranslation } from "react-i18next";
+import {
+  formatCurrency,
+  getWithExpiry,
+  setWithExpiry,
+} from "../../../logic/loigicUtils";
+import { get } from "http";
+
+const HomeSearch = () => {
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+  const { t } = useTranslation();
+  const [searchQuery, setSearchQuery] = useState("");
+  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();
+    else {
+      console.log("Areas loaded from cache:", areas.length, areas);
+      setAreasList(areas);
+    }
+  }, []);
+
+  const getAreaMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await productApi.loadArea({
+        isCountry: "-1",
+        isPopular: "-1",
+      });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopLoading());
+      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", data.data);
+        setAreasList(data.data as Area[]);
+      } else {
+        console.error("Get area failed, no token received");
+      }
+    },
+    onError: (error: any) => {
+      dispatch(stopLoading());
+      console.error("Get area error:", error.response.data);
+    },
+  });
+
+  const handleSelect = (area: Area) => {
+    console.log("Selected area:", area);
+    setIsDropdownOpen(false);
+    navigate(`/product-detail/${area.id}`, {
+      state: {
+        ...area,
+      },
+    });
+  };
+
+  const handleSearch = (query: string) => {
+    setSearchQuery(query);
+    if (query.trim() === "") {
+      // set 10 most popular areas when input is empty
+      setAreasList(areas.slice(0, 10));
+    } else {
+      // set time 500ms to wait for user to stop typing
+      const timer = setTimeout(() => {
+        // logic sau khi user ngừng gõ
+        const filtered = areas.filter((area: Area) =>
+          area.searchArea?.toLowerCase().includes(query.toLowerCase()),
+        );
+        setAreasList(filtered);
+      }, 500);
+      return () => {
+        clearTimeout(timer); // clear nếu user gõ tiếp
+      };
+    }
+  };
+
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (
+        dropdownRef.current &&
+        !dropdownRef.current.contains(event.target as Node)
+      ) {
+        setIsDropdownOpen(false);
+      }
+    };
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => {
+      document.removeEventListener("mousedown", handleClickOutside);
+    };
+  }, []);
+
+  return (
+    <section className="pt-8 pb-4 px-4 relative z-40 bg-white">
+      <div className="max-w-4xl mx-auto w-full relative">
+        <div className="flex justify-center mb-2 md:mb-2" ref={dropdownRef}>
+          <div className="w-full max-w-3xl relative group z-30">
+            <input
+              type="text"
+              placeholder={t("chooseTo")}
+              value={searchQuery}
+              onChange={(e) => {
+                handleSearch(e.target.value);
+                setIsDropdownOpen(true);
+              }}
+              onFocus={() => setIsDropdownOpen(true)}
+              className="w-full bg-white rounded-full py-4 md:py-6 px-10 md:px-12 text-base md:text-xl text-slate-700 shadow-sm border border-slate-100 outline-none focus:ring-4 focus:ring-red-50 transition-all placeholder:text-slate-300"
+            />
+            <button className="absolute right-2 md:right-3 top-2 md:top-3 bottom-2 md:bottom-3 aspect-square bg-[#EE0434] rounded-full flex items-center justify-center text-white shadow-lg shadow-red-200 hover:scale-105 transition-all">
+              <svg
+                className="w-5 h-5"
+                fill="none"
+                stroke="currentColor"
+                viewBox="0 0 24 24"
+              >
+                <path
+                  d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
+                  strokeWidth={2.5}
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                />
+              </svg>
+            </button>
+
+            {/* Dropdown Menu */}
+            {isDropdownOpen && (
+              <div className="absolute top-full left-0 right-0 mt-3 bg-white rounded-[24px] shadow-[0_20px_60px_-15px_rgba(0,0,0,0.15)] border border-slate-100 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
+                <div className="max-h-[400px] overflow-y-auto custom-scrollbar p-4">
+                  <h3 className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4 px-2 sticky top-0 bg-white z-10 py-2">
+                    {t("mostPopular")}
+                  </h3>
+                  <div className="space-y-1">
+                    {areasList && areasList.length > 0 ? (
+                      areasList?.map((p) => (
+                        <button
+                          key={p.id}
+                          onClick={() => handleSelect(p)}
+                          className="w-full flex items-center justify-between p-3 hover:bg-slate-50 rounded-2xl transition-all group"
+                        >
+                          <div className="flex items-center space-x-4">
+                            <div className="w-10 h-10 rounded-full border border-slate-100 overflow-hidden shadow-sm">
+                              <img
+                                src={`${p.iconUrl}`}
+                                alt={p.areaName1}
+                                className="w-full h-full object-cover scale-150"
+                              />
+                            </div>
+                            <span className="font-bold text-slate-700 text-lg group-hover:text-[#EE0434] transition-colors">
+                              {p.areaName1}
+                            </span>
+                          </div>
+                          <div className="text-right flex items-center space-x-2">
+                            <span className="text-sm text-slate-400 font-medium">
+                              {t("from")}:
+                            </span>
+                            <span className="text-lg font-black text-[#EE0434]">
+                              {formatCurrency(p.minSellPrice, p.curency)}
+                            </span>
+                          </div>
+                        </button>
+                      ))
+                    ) : (
+                      <div className="text-center py-8 text-slate-400 font-medium">
+                        {t("noCountryMatchesFound").replace(
+                          "{searchQuery}",
+                          searchQuery,
+                        )}
+                      </div>
+                    )}
+                  </div>
+                </div>
+              </div>
+            )}
+          </div>
+        </div>
+        <div className="flex justify-center gap-2 text-sm text-slate-400 font-medium">
+          <a
+            className="hover:text-[#EE0434] transition-colors underline color-hover cursor-pointer text-[18px] font-medium"
+            onClick={() => {
+              dispatch(openCompatibilityModal());
+            }}
+          >
+            {t("doesEsimWorkWithMyDevice")}
+          </a>
+          {/* ,
+          <button className="hover:text-[#EE0434] transition-colors">
+            Thailand
+          </button>
+          ,
+          <button className="hover:text-[#EE0434] transition-colors">
+            China
+          </button> */}
+        </div>
+      </div>
+    </section>
+  );
+};
+
+export default HomeSearch;

+ 92 - 0
EsimLao/esim-vite/src/pages/home/components/HomeTestimonial.tsx

@@ -0,0 +1,92 @@
+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";
+import { useTranslation } from "react-i18next";
+
+const HomeTestimonial = () => {
+  const dispatch = useAppDispatch();
+  const { t } = useTranslation();
+
+  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">
+      <div className="absolute top-0 right-0 w-[600px] h-[600px] bg-white/5 rounded-full blur-[100px] -translate-y-1/2 translate-x-1/2"></div>
+      <div className="absolute bottom-0 left-0 w-[400px] h-[400px] bg-black/5 rounded-full blur-[80px] translate-y-1/2 -translate-x-1/2"></div>
+
+      <div className="max-w-7xl mx-auto relative z-10 px-4 sm:px-6 lg:px-8">
+        <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 items-center">
+          <div className="lg:col-span-4 text-white space-y-4 md:space-y-6 text-center lg:text-left">
+            <h2 className="text-2xl md:text-4xl lg:text-[36px] font-black leading-tight tracking-tight">
+              {t("whatUs")}
+              {/* <br className="hidden lg:block" /> {t("sayAboutUs")} */}
+            </h2>
+            <p className="text-sm md:text-xl font-medium text-white/80 max-w-sm mx-auto lg:mx-0">
+              {t("overMillionSatisfiedCustomers")}
+            </p>
+          </div>
+
+          <div className="hidden lg:col-span-8 lg:grid grid-cols-2 gap-6 h-[550px] overflow-hidden relative">
+            <div className="absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-[#EE0434] to-transparent z-20"></div>
+            <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]">
+              {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]">
+              {loadReviewData
+                .slice()
+                .reverse()
+                .map((item, i) => (
+                  <TestimonialCard key={i} item={item} />
+                ))}
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <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">
+          {[...loadReviewData, ...loadReviewData, ...loadReviewData].map(
+            (item, i) => (
+              <div key={i} className="inline-block whitespace-normal align-top">
+                <TestimonialCard item={item} />
+              </div>
+            )
+          )}
+        </div>
+      </div>
+    </section>
+  );
+};
+
+export default HomeTestimonial;

+ 442 - 0
EsimLao/esim-vite/src/pages/login/LoginView.tsx

@@ -0,0 +1,442 @@
+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";
+import logoApp from "../../assets/img/logo-skysim.png";
+import { useTranslation } from "react-i18next";
+
+const LoginView: React.FC = () => {
+  const navigate = useNavigate();
+  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(120);
+  const intervalRef = useRef<NodeJS.Timeout | null>(null);
+  const inputRef = useRef<HTMLInputElement>(null);
+  const [errorMessage, setErrorMessage] = useState("");
+  const { t } = useTranslation();
+  let interval;
+
+  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 resetTimer = (time: number) => {
+    setTimer(time);
+
+    if (intervalRef.current) {
+      clearInterval(intervalRef.current);
+    }
+
+    intervalRef.current = setInterval(() => {
+      setTimer((prev) => (prev > 0 ? prev - 1 : 0));
+    }, 1000);
+  };
+
+  const handleResend = () => {
+    setOtp("");
+    resendOtpMutation.mutate();
+  };
+
+  const getOtpMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startSmallLoading());
+      const res = await authApi.requestOtp({ email });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopSmallLoading());
+      setErrorMessage("");
+      console.log("Get otp response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Get otp successful");
+        // show otp step
+        // Countdown timer
+        setStep("otp");
+        setErrorMessage("");
+        resetTimer(data.data.expireInSeconds || 120);
+      } 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 resendOtpMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startSmallLoading());
+      const res = await authApi.resendOtp({ email });
+      return res;
+    },
+    onSuccess: (data) => {
+      dispatch(stopSmallLoading());
+      setErrorMessage("");
+      console.log("Resend otp response data:", data);
+      if (data && data.errorCode === "0") {
+        console.log("Resend otp successful");
+        // Countdown timer
+        setErrorMessage("");
+        resetTimer(data.data.expireInSeconds || 120);
+      } else {
+        console.error("Resend 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 " + data.data.accessToken);
+        // save token to local storage
+        dispatch(accountLogin(data.data));
+        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();
+  };
+
+  const onGoogleLogin = async () => {
+    dispatch(startLoading({ message: "Redirecting to Google..." }));
+    try {
+      const res = await authApi.googleLogin();
+      if (res && res.errorCode === "0") {
+        window.location.href = res.data.url;
+      } else {
+        console.error("Google login failed:", res);
+        dispatch(stopLoading());
+      }
+    } catch (error) {
+      console.error("Google login error:", error);
+      dispatch(stopLoading());
+    }
+  };
+
+  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> */}
+
+        <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-50 h-50 flex items-center justify-center">
+                {/* <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> */}
+                <img src={logoApp} alt="SkySimHub Logo" />
+              </div>
+            </div>
+            <h1 className="text-3xl md:text-4xl lg:text-5xl font-black text-slate-900 tracking-tight">
+              {t("welcomeBack")}
+            </h1>
+            <p className="text-slate-400 font-medium text-base md:text-lg">
+              {t("stayConnected")}
+            </p>
+          </div>
+          {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">
+                    {t("emailAddress")}
+                  </label>
+                  <input
+                    type="email"
+                    value={email}
+                    onChange={(e) => setEmail(e.target.value)}
+                    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>
+                <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>{t("login")}</span>
+                </button>
+              </form>
+              <div className="space-y-6">
+                <div className="relative">
+                  <div className="absolute inset-0 flex items-center">
+                    <div className="w-full border-t border-slate-100"></div>
+                  </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]">
+                      {t("orContinueWith")}
+                    </span>
+                  </div>
+                </div>
+
+                <button
+                  type="button"
+                  onClick={onGoogleLogin}
+                  className="w-full bg-white border border-slate-200 text-slate-700 py-4 md:py-5 rounded-2xl font-bold text-lg md:text-xl shadow-sm hover:bg-slate-50 hover:border-slate-300 transition-all flex items-center justify-center space-x-3 active:scale-[0.98]"
+                >
+                  <svg className="w-6 h-6" viewBox="0 0 24 24">
+                    <path
+                      d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
+                      fill="#4285F4"
+                    />
+                    <path
+                      d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
+                      fill="#34A853"
+                    />
+                    <path
+                      d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
+                      fill="#FBBC05"
+                    />
+                    <path
+                      d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
+                      fill="#EA4335"
+                    />
+                  </svg>
+                  <span>Google</span>
+                </button>
+              </div>
+            </>
+          )}
+          {step === "otp" && (
+            <>
+              {" "}
+              {timer === 0 && 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">
+                    {t("checkYourEmail")}
+                  </h1>
+                  <p className="text-slate-400 font-medium text-base md:text-lg">
+                    {t("weveSentVerificationCode")} <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">
+                      {t("verificationCode")}
+                    </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">
+                      {t("codeExpiresIn")}{" "}
+                      <span className="text-slate-900 font-bold">{timer}s</span>
+                    </p>
+                  ) : (
+                    <p className="text-[#EE0434] font-medium animate-pulse">
+                      {t("codeExpiredPleaseRequestNewOne")}
+                    </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">{t("fastSimpleRed")}</h2>
+          <p className="text-xl opacity-80">
+            {t("globalConnectivityForTheModernTraveler")}
+          </p>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default LoginView;

+ 180 - 0
EsimLao/esim-vite/src/pages/news/NewsDetailView.tsx

@@ -0,0 +1,180 @@
+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";
+import { useTranslation } from "react-i18next";
+
+const ArticleDetailView: React.FC = () => {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const dispatch = useAppDispatch();
+  const { t } = useTranslation();
+
+  const article = location.state?.article as Article;
+  const [articleDetail, setArticleDetail] = useState<Article | null>(null);
+
+  // load article detail
+  const loadArticleDetailMutation = useMutation({
+    mutationFn: async () => {
+      dispatch(startLoading({}));
+      const res = await articleApi.LoadArticleDetail({ articleId: article.id });
+      setArticleDetail(res.data.article);
+      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:", data.message);
+      }
+    },
+    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 (
+      <div className="min-h-screen flex items-center justify-center">
+        <div className="text-center">
+          <p className="text-xl font-bold mb-4">Article not found</p>
+          <Link to="/news" className="text-[#EE0434] font-bold underline">
+            Back to News
+          </Link>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="bg-white min-h-screen pb-20">
+      <div className="max-w-7xl mx-auto px-4 py-4 md:py-6">
+        <nav className="flex items-center flex-wrap gap-2 text-xs md:text-sm text-slate-500 font-medium">
+          <Link to="/" className="hover:text-blue-500">
+            {t("home")}
+          </Link>
+          <svg
+            className="w-4 h-4"
+            fill="none"
+            stroke="currentColor"
+            viewBox="0 0 24 24"
+          >
+            <path
+              d="M9 5l7 7-7 7"
+              strokeWidth={2}
+              strokeLinecap="round"
+              strokeLinejoin="round"
+            />
+          </svg>
+          <Link to="/news" className="hover:text-blue-500">
+            {t("news")}
+          </Link>
+          <svg
+            className="w-4 h-4"
+            fill="none"
+            stroke="currentColor"
+            viewBox="0 0 24 24"
+          >
+            <path
+              d="M9 5l7 7-7 7"
+              strokeWidth={2}
+              strokeLinecap="round"
+              strokeLinejoin="round"
+            />
+          </svg>
+          <span className="text-slate-900 font-bold truncate max-w-[200px] md:max-w-none">
+            {article.title}
+          </span>
+        </nav>
+      </div>
+
+      <div className="max-w-7xl mx-auto px-4">
+        <header className="py-6 md:py-10 border-t border-slate-50">
+          <h1 className="text-2xl md:text-3xl lg:text-4xl font-black text-[#EE0434] leading-tight mb-4">
+            {article.title}
+          </h1>
+          <div className="flex items-center text-slate-400 space-x-2">
+            <svg
+              className="w-4 h-4"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                strokeLinecap="round"
+                strokeLinejoin="round"
+                strokeWidth={2}
+                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.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={articleDetail?.coverImageUrl}
+              alt={article.title}
+              className="w-full h-full object-cover"
+            />
+          </div>
+        </div>
+
+        <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}
+            </h2>
+            {/* <p className="text-base md:text-lg leading-relaxed mb-4 font-medium text-slate-600"> */}
+            {/* show html  */}
+            <div
+              className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8"
+              dangerouslySetInnerHTML={{ __html: articleDetail?.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">
+            <button
+              onClick={() => navigate("/news")}
+              className="flex items-center space-x-2 text-[#EE0434] font-black hover:translate-x-[-4px] transition-transform text-sm md:text-base"
+            >
+              <svg
+                className="w-4 h-4"
+                fill="none"
+                stroke="currentColor"
+                viewBox="0 0 24 24"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={3}
+                  d="M15 19l-7-7 7-7"
+                />
+              </svg>
+              <span>{t("backToNews")}</span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ArticleDetailView;

Деякі файли не було показано, через те що забагато файлів було змінено