| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- import React, { useState, useMemo, useEffect } from "react";
- import { useLocation, useNavigate, Link } from "react-router-dom";
- import { SelectedProduct } from "../../services/types";
- import { Area, Package } from "../../services/product/type";
- import { useMutation, useQuery } from "@tanstack/react-query";
- import { DataCacheKey, staleTime } from "../../global/constants";
- import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
- import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
- import { productApi } from "../../apis/productApi";
- import { openPopup } from "../../features/popup/popupSlice";
- import { get } from "http";
- const ProductDetailView: React.FC = () => {
- const location = useLocation();
- const navigate = useNavigate();
- const dispatch = useAppDispatch();
- const area = location.state as Area;
- const loading = useAppSelector((state) => state.loading);
- const [selectedDays, setSelectedDays] = useState<number>(null);
- const [selectedData, setSelectedData] = useState<string>("Unlimited");
- const [daysOptions, setDaysOptions] = useState<number[]>([]);
- const [dataOptions, setDataOptions] = useState<string[]>([]);
- const [daysActiveOptions, setDaysActiveOptions] = useState<number[]>([]);
- const [dataActiveOptions, setDataActiveOptions] = useState<string[]>([]);
- const [prices, setPrices] = useState<{
- original: string;
- final: string;
- discountPercent: string;
- }>({
- original: "0.00",
- final: "0.00",
- discountPercent: "0",
- });
- const [simType, setSimType] = useState<"eSIM" | "Physical">("eSIM");
- const [quantity, setQuantity] = useState<number>(1);
- const [packages, setPackages] = useState<Package[]>(null);
- if (!area) {
- return (
- <div className="min-h-screen flex items-center justify-center">
- <div className="text-center">
- <p className="text-xl font-bold mb-4">Product details missing</p>
- <Link to="/buy-sim" className="text-[#EE0434] font-bold underline">
- Go to Shop
- </Link>
- </div>
- </div>
- );
- }
- // const { data: loadPackage = [] } = useQuery<Package[]>({
- // queryKey: [DataCacheKey.PACKAGES],
- // queryFn: async (): Promise<Package[]> => {
- // try {
- // dispatch(startLoading({}));
- // const res = await productApi.loadPackage({
- // areaId: area.id,
- // isUnlimited: "-1",
- // isDaily: "-1",
- // });
- // return res.data as Package[];
- // } catch (error) {
- // console.error(error);
- // return []; // 🔴 bắt buộc
- // } finally {
- // dispatch(stopLoading());
- // }
- // },
- // staleTime: staleTime,
- // });
- useEffect(() => {
- getProductMutation.mutate();
- }, []);
- const getProductMutation = useMutation({
- mutationFn: async () => {
- dispatch(startLoading({}));
- const res = await productApi.loadPackage({
- areaId: area.id,
- isUnlimited: "-1",
- isDaily: "-1",
- });
- return res;
- },
- onSuccess: (data) => {
- dispatch(stopLoading());
- console.log("Get package response data:", data);
- if (data && data.errorCode === "0") {
- console.log("Get package successful");
- setPackages(data.data as Package[]);
- } else {
- console.error("Get package failed, no token received");
- }
- },
- onError: (error: any) => {
- dispatch(stopLoading());
- console.error("Get package error:", error.response.data);
- },
- });
- const options = useMemo(() => {
- console.log("Calculating options from loadPackage");
- const daysSet = new Set<number>();
- const dataSet = new Set<string>();
- packages.forEach((p) => {
- daysSet.add(p.dayDuration);
- dataSet.add(p.amountData.toString());
- });
- const daysArray = Array.from(daysSet).sort((a, b) => a - b);
- const dataArray = Array.from(dataSet).sort((a, b) => {
- if (a === "Unlimited") return 1;
- if (b === "Unlimited") return -1;
- return parseInt(a) - parseInt(b);
- });
- return {
- daysArray,
- dataArray,
- };
- }, [packages]);
- useEffect(() => {
- setDaysOptions(options.daysArray);
- setDataOptions(options.dataArray);
- handleSelectDay(options.daysArray[0]);
- handleSelectData(options.dataArray[0]);
- }, [options]);
- useEffect(() => {
- getPrices();
- }, [selectedDays, selectedData]);
- const handleSelectDay = (day: number) => {
- // filter data options based on selected day if needed
- const dataSet = new Set<string>();
- packages.forEach((p) => {
- if (p.dayDuration === day) {
- dataSet.add(p.amountData.toString());
- }
- });
- setSelectedDays(day);
- setDataActiveOptions(Array.from(dataSet));
- };
- const handleSelectData = (data: string) => {
- // filter day options based on selected data if needed
- const daysSet = new Set<number>();
- packages.forEach((p) => {
- if (p.amountData.toString() === data) {
- daysSet.add(p.dayDuration);
- }
- });
- setSelectedData(data);
- setDaysActiveOptions(Array.from(daysSet));
- };
- const getPrices = (quantityParam?: number) => {
- const quantityToUse =
- quantityParam !== undefined ? quantityParam : quantity;
- // find package based on selectedDays and selectedData
- const selectedPackage = packages.find(
- (p) =>
- p.dayDuration === selectedDays &&
- p.amountData.toString() === selectedData
- );
- if (!selectedPackage) {
- console.log(
- "No package found for the selected options " +
- selectedDays +
- " days and " +
- selectedData +
- " data"
- );
- return {
- original: "0.00",
- final: "0.00",
- discountPercent: "0",
- };
- }
- console.log(
- "Selected package: ",
- selectedPackage +
- " for prices calculation " +
- selectedPackage.sellPrice +
- " quantity " +
- quantityToUse
- );
- setPrices({
- original: quantityToUse * selectedPackage.displayPrice,
- final: quantityToUse * selectedPackage.sellPrice,
- discountPercent: "0",
- });
- };
- const handleQuantityChange = (change: number) => {
- const newQuantity = Math.max(1, quantity + change);
- setQuantity(newQuantity);
- console.log("Selected quantity: ", newQuantity);
- getPrices(newQuantity);
- };
- const handleBuyNow = async () => {
- // Logic for custom order or standard buy
- console.log("Buy now clicked");
- const selectedPackage = packages.find(
- (p) =>
- p.dayDuration === selectedDays &&
- p.amountData.toString() === selectedData
- );
- if (!selectedPackage) {
- alert("Please select a valid package");
- return;
- }
- // call logic to proceed to checkout
- const res = await productApi.checkout({
- packgId: selectedPackage.id,
- quantity: quantity,
- });
- if (res && res.errorCode === "0") {
- console.log("Checkout details loaded:", res.data);
- // navigate to checkout with selected options
- navigate("/checkout", {
- state: {
- area: area,
- package: selectedPackage,
- quantity: quantity,
- simType: simType,
- checkoutDetails: res.data,
- },
- });
- } else {
- console.error("Failed to load checkout details:", res.message);
- dispatch(
- openPopup({
- isSuccess: false,
- title: "Checkout Error",
- message: res.message || "Failed to proceed to checkout.",
- buttonText: "Close",
- })
- );
- }
- };
- 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 space-x-2 text-xs md:text-sm text-slate-500">
- <Link
- to="/"
- className="hover:text-[#EE0434] transition-colors text-[18px]"
- >
- Home
- </Link>
- <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]">
- {area.areaName1}
- </span>
- </nav>
- </div>
- <div className="max-w-7xl mx-auto px-4 grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
- <div className="lg:col-span-5 space-y-6">
- <div className="relative aspect-[3/4] rounded-[24px] md:rounded-[32px] overflow-hidden shadow-2xl group">
- <img
- src={area.imgUrl}
- alt={area.areaName1}
- className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
- />
- <div className="absolute top-6 left-6 flex items-start space-x-4">
- <div className="w-12 h-12 md:w-16 md:h-16 rounded-full overflow-hidden border-2 border-white/50 shadow-lg">
- <img
- src={`${area.iconUrl}`}
- alt={area.areaName1}
- className="w-full h-full object-cover scale-150"
- />
- </div>
- <div className="text-white">
- <h1 className="text-2xl md:text-4xl font-black drop-shadow-md">
- SIM {area.areaName1}
- </h1>
- <p className="text-sm md:text-lg font-bold text-white/90">
- Verified: <span className="text-[#EE0434]">High Speed</span>
- </p>
- </div>
- </div>
- </div>
- </div>
- <div className="lg:col-span-7 space-y-8 md:space-y-10 pt-4">
- <div className="space-y-4">
- <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
- Number of days
- </h3>
- <div className="flex flex-wrap gap-3">
- {daysOptions.map((day) => (
- <button
- key={day}
- onClick={() =>
- daysActiveOptions.includes(day) && handleSelectDay(day)
- }
- 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 ${
- selectedDays === day
- ? "border-[#EE0434] text-white bg-[#EE0434] shadow-md"
- : daysActiveOptions.includes(day)
- ? "border-[#ffffff] text-black bg-[#ffffff] shadow-md"
- : "border-slate-100 text-slate-300"
- }`}
- >
- {day}
- </button>
- ))}
- </div>
- </div>
- <div className="space-y-4">
- <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
- Data
- </h3>
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
- {dataOptions.map((data) => (
- <button
- key={data}
- onClick={() =>
- dataActiveOptions.includes(data) && handleSelectData(data)
- }
- className={`h-10 md:h-14 rounded-xl md:rounded-2xl font-bold text-sm md:text-xl transition-all border-2 ${
- selectedData === data
- ? "border-[#EE0434] text-white bg-[#EE0434] shadow-md"
- : dataActiveOptions.includes(data)
- ? "border-[#ffffff] text-black bg-[#ffffff] shadow-md"
- : "border-slate-100 text-slate-300"
- }`}
- >
- {data === "0" ? "Unlimited" : data + " MB"}
- </button>
- ))}
- </div>
- </div>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-4">
- <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
- SIM Type
- </h3>
- <div className="flex p-1 bg-slate-50 rounded-2xl border border-slate-100">
- <button
- onClick={() => setSimType("eSIM")}
- className={`flex-1 py-3 md:py-4 rounded-xl font-black text-sm md:text-2xl transition-all ${
- simType === "eSIM"
- ? "bg-[#EE0434] text-white shadow-lg"
- : "text-slate-300"
- }`}
- >
- eSIM
- </button>
- <button
- onClick={() => setSimType("Physical")}
- className={`flex-1 py-3 md:py-4 rounded-xl font-black text-sm md:text-2xl transition-all ${
- simType === "Physical"
- ? "bg-[#EE0434] text-white shadow-lg"
- : "text-slate-300"
- }`}
- >
- Physical SIM
- </button>
- </div>
- </div>
- <div className="space-y-4">
- <h3 className="text-lg md:text-xl font-black text-slate-900 tracking-tight">
- Quantity
- </h3>
- <div className="flex items-center space-x-4 p-2 bg-slate-50 rounded-2xl border border-slate-100 h-[68px] md:h-[76px]">
- <button
- onClick={() => handleQuantityChange(-1)}
- className="w-12 h-full bg-white rounded-xl shadow-sm text-slate-600 font-bold text-2xl hover:bg-slate-100 transition-colors"
- >
- -
- </button>
- <span className="flex-1 text-center font-black text-2xl text-slate-900">
- {quantity}
- </span>
- <button
- onClick={() => handleQuantityChange(1)}
- className="w-12 h-full bg-white rounded-xl shadow-sm text-slate-600 font-bold text-2xl hover:bg-slate-100 transition-colors"
- >
- +
- </button>
- </div>
- </div>
- </div>
- <div className="bg-red-50/50 border border-[#EE0434]/20 rounded-[24px] md:rounded-[40px] p-6 md:p-10 space-y-6">
- <div className="flex justify-between items-end">
- <div className="flex items-center space-x-2">
- <span className="text-[#EE0434] font-black text-lg">
- -{prices.discountPercent}%
- </span>
- <span className="text-slate-300 font-bold text-xs line-through">
- {prices.original.toLocaleString("vi-VN", {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}{" "}
- {area.curency}
- </span>
- </div>
- <span className="text-[#EE0434] font-black text-2xl md:text-3xl">
- {prices.final.toLocaleString("vi-VN", {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}{" "}
- {area.curency}
- </span>
- </div>
- <button
- disabled={selectedData && selectedDays && 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 ${
- selectedData && selectedDays && !loading.isSmallLoading
- ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
- : "bg-slate-100 text-slate-300 cursor-not-allowed"
- }`}
- onClick={handleBuyNow}
- >
- {loading.isSmallLoading && (
- <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
- )}
- <span>Buy now</span>
- </button>
- </div>
- </div>
- </div>
- </div>
- );
- };
- export default ProductDetailView;
|