ProductDetailView.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. import React, { useState, useMemo, useEffect } from "react";
  2. import { useLocation, useNavigate, Link, useParams } from "react-router-dom";
  3. import { SelectedProduct } from "../../services/types";
  4. import { Area, Package } from "../../services/product/type";
  5. import { useMutation, useQuery } from "@tanstack/react-query";
  6. import { DataCacheKey, staleTime } from "../../global/constants";
  7. import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
  8. import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
  9. import { productApi } from "../../apis/productApi";
  10. import { openPopup } from "../../features/popup/popupSlice";
  11. import {
  12. formatCurrency,
  13. formatNumber,
  14. getWithExpiry,
  15. } from "../../logic/loigicUtils";
  16. import ProductInfoModal from "../../components/ProductInfoModal";
  17. import PackageOverview from "./components/PackageOverview";
  18. import ProductCard from "../../components/ProductCard";
  19. import { useTranslation } from "react-i18next";
  20. import { get } from "http";
  21. const ProductDetailView: React.FC = () => {
  22. const location = useLocation();
  23. const navigate = useNavigate();
  24. const dispatch = useAppDispatch();
  25. const { t } = useTranslation();
  26. // let area = location.state as Area;
  27. const areas = getWithExpiry<Area[]>("areas");
  28. const { id } = useParams<{ id: string }>();
  29. const loading = useAppSelector((state) => state.loading);
  30. const [selectedDays, setSelectedDays] = useState<number>(null);
  31. const [selectedData, setSelectedData] = useState<string>("Unlimited");
  32. const [daysOptions, setDaysOptions] = useState<number[]>([]);
  33. const [dataOptions, setDataOptions] = useState<string[]>([]);
  34. const [daysActiveOptions, setDaysActiveOptions] = useState<number[]>([]);
  35. const [dataActiveOptions, setDataActiveOptions] = useState<string[]>([]);
  36. const [relatedAreas, setRelatedAreas] = useState<Area[]>([]);
  37. const [prices, setPrices] = useState<{
  38. original: string;
  39. final: string;
  40. discountPercent: string;
  41. }>({
  42. original: "0.00",
  43. final: "0.00",
  44. discountPercent: "0",
  45. });
  46. const [simType, setSimType] = useState<"eSIM" | "Physical">("eSIM");
  47. const [quantity, setQuantity] = useState<number>(1);
  48. const [packages, setPackages] = useState<Package[]>([]);
  49. const [area, setArea] = useState<Area | null>(null);
  50. const [selectedPackage, setSelectedPackage] = useState<Package | null>(null);
  51. useEffect(() => {
  52. console.log("ProductDetailView loaded with id:", id);
  53. if (areas && areas.length > 0) {
  54. const areaTmp = areas.find((a) => a.id.toString() === id.toString());
  55. setArea(areaTmp);
  56. getProductMutation.mutate();
  57. getRelatedAreaMutation.mutate();
  58. console.log("Set area from cache:", areaTmp);
  59. }
  60. }, [id]);
  61. const getProductMutation = useMutation({
  62. mutationFn: async () => {
  63. dispatch(startLoading({}));
  64. const res = await productApi.loadPackage({
  65. areaId: area?.id,
  66. dataType: "-1",
  67. });
  68. return res;
  69. },
  70. onSuccess: (data) => {
  71. dispatch(stopLoading());
  72. console.log("Get package response data:", data);
  73. if (data && data.errorCode === "0") {
  74. const packages = data.data as Package[];
  75. setPackages(packages);
  76. } else {
  77. console.error("Get package failed, no token received");
  78. }
  79. },
  80. onError: (error: any) => {
  81. dispatch(stopLoading());
  82. console.error("Get package error:", error.response.data);
  83. },
  84. });
  85. const getRelatedAreaMutation = useMutation({
  86. mutationFn: async () => {
  87. dispatch(startLoading({}));
  88. const res = await productApi.loadRelatedArea({
  89. areaId: id,
  90. });
  91. return res;
  92. },
  93. onSuccess: (data) => {
  94. dispatch(stopLoading());
  95. console.log("Get package response data:", data);
  96. if (data && data.errorCode === "0") {
  97. const areas = data.data as Area[];
  98. setRelatedAreas(areas);
  99. } else {
  100. console.error("Get package failed, no token received");
  101. }
  102. },
  103. onError: (error: any) => {
  104. dispatch(stopLoading());
  105. console.error("Get package error:", error.response.data);
  106. },
  107. });
  108. const convertPackageToSelectedProduct = (packg: Package) => {
  109. return packg.title;
  110. };
  111. const options = useMemo(() => {
  112. console.log("Calculating options from loadPackage");
  113. const daysSet = new Set<number>();
  114. const dataSet = new Set<string>();
  115. packages.forEach((p) => {
  116. daysSet.add(p.dayDuration);
  117. dataSet.add(convertPackageToSelectedProduct(p));
  118. });
  119. const daysArray = Array.from(daysSet).sort((a, b) => a - b);
  120. // const dataArray = Array.from(dataSet).sort((a, b) => {
  121. // if (a === "Unlimited") return 1;
  122. // if (b === "Unlimited") return -1;
  123. // return parseInt(a) - parseInt(b);
  124. // });
  125. return {
  126. daysArray,
  127. dataArray: Array.from(dataSet),
  128. };
  129. }, [packages]);
  130. useEffect(() => {
  131. setDaysOptions(options.daysArray);
  132. setDataOptions(options.dataArray);
  133. handleSelectDay(options.daysArray[0]);
  134. handleSelectData(options.dataArray[0]);
  135. // setSelectedPackage(packages.length > 0 ? packages[0] : null);
  136. // console.log("Set package successful", packages.length, selectedPackage);
  137. }, [options]);
  138. useEffect(() => {
  139. getPrices();
  140. }, [selectedDays, selectedData]);
  141. const handleSelectDay = (day: number) => {
  142. // filter data options based on selected day if needed
  143. const dataSet = new Set<string>();
  144. packages.forEach((p) => {
  145. if (p.dayDuration === day) {
  146. dataSet.add(convertPackageToSelectedProduct(p));
  147. }
  148. });
  149. setSelectedDays(day);
  150. setSelectedData(
  151. dataSet.has(selectedData) ? selectedData : Array.from(dataSet)[0],
  152. );
  153. setDataActiveOptions(Array.from(dataSet));
  154. setSelectedPackage(
  155. packages.find(
  156. (p) =>
  157. p.dayDuration === selectedDays &&
  158. convertPackageToSelectedProduct(p) === selectedData,
  159. ),
  160. );
  161. };
  162. const handleSelectData = (data: string) => {
  163. // filter day options based on selected data if needed
  164. const daysSet = new Set<number>();
  165. packages.forEach((p) => {
  166. if (convertPackageToSelectedProduct(p) === data) {
  167. daysSet.add(p.dayDuration);
  168. }
  169. });
  170. setSelectedData(data);
  171. setSelectedDays(
  172. daysSet.has(selectedDays) ? selectedDays : Array.from(daysSet)[0],
  173. );
  174. setDaysActiveOptions(Array.from(daysSet));
  175. setSelectedPackage(
  176. packages.find(
  177. (p) =>
  178. p.dayDuration === selectedDays &&
  179. convertPackageToSelectedProduct(p) === selectedData,
  180. ),
  181. );
  182. };
  183. const getPrices = (quantityParam?: number) => {
  184. const quantityToUse =
  185. quantityParam !== undefined ? quantityParam : quantity;
  186. // find package based on selectedDays and selectedData
  187. let selectedPackageTmp = packages.find(
  188. (p) =>
  189. p.dayDuration === selectedDays &&
  190. convertPackageToSelectedProduct(p) === selectedData,
  191. );
  192. if (!selectedPackageTmp) {
  193. // console.log(
  194. // "No package found for the selected options " +
  195. // selectedDays +
  196. // " days and " +
  197. // selectedData +
  198. // " data"
  199. // );
  200. return {
  201. original: "0.00",
  202. final: "0.00",
  203. discountPercent: "0",
  204. };
  205. }
  206. console.log(
  207. "Selected package: ",
  208. selectedPackageTmp + " quantity " + quantityToUse,
  209. );
  210. setPrices({
  211. original: quantityToUse * selectedPackageTmp.displayPrice,
  212. final: quantityToUse * selectedPackageTmp.sellPrice,
  213. discountPercent: selectedPackageTmp.discountPercent,
  214. });
  215. setSelectedPackage(selectedPackageTmp);
  216. };
  217. const handleQuantityChange = (change: number) => {
  218. const newQuantity = Math.max(1, quantity + change);
  219. setQuantity(newQuantity);
  220. console.log("Selected quantity: ", newQuantity);
  221. getPrices(newQuantity);
  222. };
  223. const handleBuyNow = async () => {
  224. // Logic for custom order or standard buy
  225. console.log("Buy now clicked");
  226. const selectedPackage = packages.find(
  227. (p) =>
  228. p.dayDuration === selectedDays &&
  229. convertPackageToSelectedProduct(p) === selectedData,
  230. );
  231. if (!selectedPackage) {
  232. alert("Please select a valid package");
  233. return;
  234. }
  235. // call logic to proceed to checkout
  236. const res = await productApi.checkout({
  237. packgId: selectedPackage.id,
  238. quantity: quantity,
  239. });
  240. if (res && res.errorCode === "0") {
  241. console.log("Checkout details loaded:", res.data);
  242. // navigate to checkout with selected options
  243. navigate("/checkout", {
  244. state: {
  245. area: area,
  246. package: selectedPackage,
  247. quantity: quantity,
  248. simType: simType,
  249. checkoutDetails: res.data,
  250. },
  251. });
  252. } else {
  253. console.error("Failed to load checkout details:", res.message);
  254. dispatch(
  255. openPopup({
  256. isSuccess: false,
  257. title: "Checkout Error",
  258. message: res.message || "Failed to proceed to checkout.",
  259. buttonText: "Close",
  260. }),
  261. );
  262. }
  263. };
  264. if (!id || !area) {
  265. return (
  266. <div className="min-h-screen flex items-center justify-center">
  267. <div className="text-center">
  268. <p className="text-xl font-bold mb-4">{t("productDetailsMissing")}</p>
  269. <Link to="/buy-sim" className="text-[#EE0434] font-bold underline">
  270. {t("goToShop")}
  271. </Link>
  272. </div>
  273. </div>
  274. );
  275. }
  276. return (
  277. <div className="bg-white min-h-screen pb-20">
  278. <div className="max-w-7xl mx-auto px-4 py-4 md:py-6">
  279. <nav className="flex items-center space-x-2 text-xs md:text-sm text-slate-500">
  280. <Link
  281. to="/"
  282. className="hover:text-[#EE0434] transition-colors text-[18px]"
  283. >
  284. {t("home")}
  285. </Link>
  286. <svg
  287. className="w-3 h-3"
  288. fill="none"
  289. stroke="currentColor"
  290. viewBox="0 0 24 24"
  291. >
  292. <path
  293. d="M9 5l7 7-7 7"
  294. strokeWidth={2.5}
  295. strokeLinecap="round"
  296. strokeLinejoin="round"
  297. />
  298. </svg>
  299. <span className="text-slate-900 font-bold text-[18px]">
  300. {area?.areaName1}
  301. </span>
  302. </nav>
  303. </div>
  304. <div className="max-w-7xl mx-auto px-4 grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
  305. <div className="lg:col-span-5 space-y-6">
  306. <div className="relative aspect-[3/4] rounded-[24px] md:rounded-[32px] overflow-hidden shadow-2xl group">
  307. <img
  308. src={area?.imgUrl}
  309. alt={area?.areaName1}
  310. className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
  311. />
  312. <div className="absolute top-6 left-6 flex items-start space-x-4">
  313. <div className="w-12 h-12 md:w-16 md:h-16 rounded-full overflow-hidden border-2 border-white/50 shadow-lg">
  314. <img
  315. src={`${area?.iconUrl}`}
  316. alt={area?.areaName1}
  317. className="w-full h-full object-cover scale-150"
  318. />
  319. </div>
  320. <div className="text-white">
  321. <h1 className="text-2xl md:text-4xl font-black drop-shadow-md">
  322. SIM {area?.areaName1}
  323. </h1>
  324. <p className="text-sm md:text-lg font-bold text-white/90">
  325. {t("verified")}:{" "}
  326. <span className="text-[#EE0434]">{t("highSpeed")}</span>
  327. </p>
  328. </div>
  329. </div>
  330. </div>
  331. {/* Product Information Card */}
  332. <PackageOverview packageInfo={selectedPackage} />
  333. </div>
  334. <div className="lg:col-span-7 space-y-8 md:space-y-10 pt-4">
  335. <div className="space-y-4">
  336. <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
  337. {t("numberOfDays")}
  338. </h3>
  339. <div className="flex flex-wrap gap-3">
  340. {daysOptions.map((day) => (
  341. <button
  342. key={day}
  343. onClick={() =>
  344. // daysActiveOptions.includes(day) &&
  345. handleSelectDay(day)
  346. }
  347. className={`min-w-[50px] md:min-w-[70px] h-10 md:h-14 rounded-xl md:rounded-2xl font-bold text-base md:text-xl transition-all border-2 ${
  348. selectedDays === day
  349. ? "border-[#EE0434] text-white bg-[#EE0434] shadow-md"
  350. : daysActiveOptions.includes(day)
  351. ? "border-[#ffffff] text-black bg-[#ffffff] shadow-md"
  352. : "border-slate-100 text-slate-300"
  353. }`}
  354. >
  355. {day}
  356. </button>
  357. ))}
  358. </div>
  359. </div>
  360. <div className="space-y-4">
  361. <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
  362. Data
  363. </h3>
  364. <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
  365. {dataOptions.map((data) => (
  366. <button
  367. key={data}
  368. onClick={() =>
  369. // dataActiveOptions.includes(data) &&
  370. handleSelectData(data)
  371. }
  372. className={`h-10 md:h-14 rounded-xl md:rounded-2xl font-bold text-sm md:text-xl transition-all border-2 ${
  373. selectedData === data
  374. ? "border-[#EE0434] text-white bg-[#EE0434] shadow-md"
  375. : dataActiveOptions.includes(data)
  376. ? "border-[#ffffff] text-black bg-[#ffffff] shadow-md"
  377. : "border-slate-100 text-slate-300"
  378. }`}
  379. >
  380. {data === "0" ? "Unlimited" : data}
  381. </button>
  382. ))}
  383. </div>
  384. </div>
  385. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  386. <div className="space-y-4">
  387. <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
  388. {t("simType")}
  389. </h3>
  390. <div className="flex p-1 bg-slate-50 rounded-2xl border border-slate-100">
  391. <button
  392. onClick={() => setSimType("eSIM")}
  393. className={`flex-1 py-3 md:py-4 rounded-xl font-black text-sm md:text-2xl transition-all ${
  394. simType === "eSIM"
  395. ? "bg-[#EE0434] text-white shadow-lg"
  396. : "text-slate-300"
  397. }`}
  398. >
  399. eSIM
  400. </button>
  401. {/* <button
  402. onClick={() => setSimType("Physical")}
  403. className={`flex-1 py-3 md:py-4 rounded-xl font-black text-sm md:text-2xl transition-all ${
  404. simType === "Physical"
  405. ? "bg-[#EE0434] text-white shadow-lg"
  406. : "text-slate-300"
  407. }`}
  408. >
  409. Physical SIM
  410. </button> */}
  411. </div>
  412. </div>
  413. <div className="space-y-4">
  414. <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
  415. {t("quantity")}
  416. </h3>
  417. <div className="flex items-center space-x-4 p-2 bg-slate-50 rounded-2xl border border-slate-100 h-[68px] md:h-[76px]">
  418. <button
  419. onClick={() => handleQuantityChange(-1)}
  420. className="w-12 h-full bg-white rounded-xl shadow-sm text-slate-600 font-bold text-2xl hover:bg-slate-100 transition-colors"
  421. >
  422. -
  423. </button>
  424. <span className="flex-1 text-center font-black text-2xl text-slate-900">
  425. {quantity}
  426. </span>
  427. <button
  428. onClick={() => handleQuantityChange(1)}
  429. className="w-12 h-full bg-white rounded-xl shadow-sm text-slate-600 font-bold text-2xl hover:bg-slate-100 transition-colors"
  430. >
  431. +
  432. </button>
  433. </div>
  434. </div>
  435. </div>
  436. <div className="bg-red-50/50 border border-[#EE0434]/20 rounded-[24px] md:rounded-[40px] p-6 md:p-10 space-y-6">
  437. <div className="flex justify-between items-end">
  438. <div className="flex items-center space-x-2">
  439. <span className="text-[#EE0434] font-black text-lg">
  440. {prices.discountPercent}%
  441. </span>
  442. <span className="text-slate-300 font-bold text-xs line-through">
  443. {formatCurrency(prices.original, area?.curency)}
  444. </span>
  445. </div>
  446. <span className="text-[#EE0434] font-black text-2xl md:text-3xl">
  447. {formatCurrency(prices.final, area?.curency)}
  448. </span>
  449. </div>
  450. <button
  451. disabled={selectedData && selectedDays && loading.isSmallLoading}
  452. 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 ${
  453. selectedData && selectedDays && !loading.isSmallLoading
  454. ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
  455. : "bg-slate-100 text-slate-300 cursor-not-allowed"
  456. }`}
  457. onClick={handleBuyNow}
  458. >
  459. {loading.isSmallLoading && (
  460. <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
  461. )}
  462. <span>{t("buyNow")}</span>
  463. </button>
  464. </div>
  465. {/* Suggestions Section */}
  466. <div className="mt-12">
  467. <h3 className="text-xl md:text-2xl font-black text-slate-900 mb-6">
  468. {t("suggestionsEsim")} {area.areaName1}:
  469. </h3>
  470. <div className="grid grid-cols-2 gap-3 md:gap-4">
  471. {relatedAreas.map((item) => (
  472. <ProductCard
  473. key={item.id}
  474. p={item}
  475. onClick={() => {
  476. navigate(`/product-detail/${item.id}`, {
  477. state: {
  478. ...item,
  479. },
  480. });
  481. window.scrollTo({ top: 0, behavior: "smooth" });
  482. }}
  483. />
  484. ))}
  485. </div>
  486. </div>
  487. </div>
  488. </div>
  489. </div>
  490. );
  491. };
  492. export default ProductDetailView;