Checkout Page Component Implementation

This is the initial setup for my React checkout page. It includes hooks for cart management, authentication, and framer-motion for animations. I'm also setting up utility functions for handling image URLs and processing payments.
user_3179742 avatar
user_3179742
jsx
02/10/2026 7:26 AM
63.5 KB
11
Indexable
// File: src/pages/CheckoutPage/CheckoutPage.jsx
import React, { useState, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useCart } from "../../features/cart/hooks/useCart";
import { useAlert } from "../../components/common/Alert/AlertManager";
import { useAuth } from "../../features/auth/hooks/useAuth";
import { motion, AnimatePresence } from "framer-motion";
import { processOnlinePayment, redirectToPayment } from "../../utils/paymentUtils";
import {
  FaArrowLeft,
  FaMapMarkerAlt,
  FaCreditCard,
  FaLock,
  FaCheck,
  FaEdit,
  FaTruck,
  FaMoneyBillWave,
  FaShieldAlt,
  FaChevronDown,
  FaHome,
  FaBriefcase,
  FaExclamationTriangle
} from "react-icons/fa";
import { addressService } from "../../features/user/services/addressService";

const formatImageUrl = (url) => {
  if (!url) return "/images/product-placeholder.jpg";
  if (url.startsWith('http')) return url;
  if (url.startsWith('/uploads')) return `/api${url}`;
  return `/api/uploads/${url}`;
};

// Mobile Delivery Address Component
const MobileDeliveryAddress = ({ selectedAddress, addresses, loading, apiError, onOpenModal, onRetry, onSetDefault }) => {
  const getAddressIcon = (type) => {
    switch (type) {
      case "home":
        return <FaHome className="text-primary-green" />;
      case "work":
        return <FaBriefcase className="text-blue-500" />;
      default:
        return <FaMapMarkerAlt className="text-gray-500" />;
    }
  };

  const getTypeLabel = (type) => {
    switch (type) {
      case "home":
        return "Home";
      case "work":
        return "Work";
      default:
        return "Other";
    }
  };

  if (loading) {
    return (
      <div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
        <div className="flex items-center justify-between mb-3">
          <h3 className="text-base font-bold text-dark-charcoal flex items-center gap-2">
            <FaMapMarkerAlt className="text-primary-green text-sm" />
            Delivery Address
          </h3>
          <div className="w-20 h-7 bg-gray-200 rounded animate-pulse"></div>
        </div>
        <div className="space-y-2">
          <div className="h-3 bg-gray-200 rounded w-1/3 animate-pulse"></div>
          <div className="h-3 bg-gray-200 rounded w-2/3 animate-pulse"></div>
          <div className="h-3 bg-gray-200 rounded w-1/2 animate-pulse"></div>
        </div>
      </div>
    );
  }

  if (apiError && addresses.length === 0) {
    return (
      <div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
        <h3 className="text-base font-bold text-dark-charcoal flex items-center gap-2 mb-3">
          <FaMapMarkerAlt className="text-primary-green text-sm" />
          Delivery Address
        </h3>
        <div className="text-center py-4">
          <FaExclamationTriangle className="mx-auto text-yellow-500 text-xl mb-2" />
          <p className="text-gray-600 text-sm mb-3">{apiError}</p>
          <button
            onClick={onRetry}
            className="bg-primary-green text-white px-4 py-2 rounded-lg text-sm hover:bg-dark-green transition-all w-full"
          >
            Try Again
          </button>
        </div>
      </div>
    );
  }

  if (addresses.length === 0 && !loading) {
    return (
      <div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
        <h3 className="text-base font-bold text-dark-charcoal flex items-center gap-2 mb-3">
          <FaMapMarkerAlt className="text-primary-green text-sm" />
          Delivery Address
        </h3>
        <div className="text-center py-4">
          <FaMapMarkerAlt className="mx-auto text-gray-400 text-2xl mb-2" />
          <h4 className="font-semibold text-dark-charcoal text-sm mb-1">
            No delivery addresses
          </h4>
          <p className="text-gray-500 text-xs mb-3">
            Please add a delivery address to continue
          </p>
          <Link
            to="/profile/addresses"
            className="block bg-primary-green text-white px-4 py-2 rounded-lg text-sm hover:bg-dark-green transition-all"
          >
            Add Address
          </Link>
        </div>
      </div>
    );
  }

  return (
    <div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
      <div className="flex items-center justify-between mb-3">
        <h3 className="text-base font-bold text-dark-charcoal flex items-center gap-2">
          <FaMapMarkerAlt className="text-primary-green text-sm" />
          Delivery Address
        </h3>
        <button
          onClick={onOpenModal}
          className="flex items-center gap-1 px-3 py-1.5 text-primary-green border border-primary-green rounded-lg font-medium text-xs hover:bg-primary-green/5 transition-all"
        >
          <FaEdit className="text-xs" />
          Change
        </button>
      </div>

      {selectedAddress && (
        <div>
          <div className="flex items-center gap-2 mb-1">
            <p className="font-semibold text-dark-charcoal text-sm">
              {selectedAddress.name}
            </p>
            {selectedAddress.isDefault && (
              <span className="px-2 py-0.5 bg-primary-green/10 text-primary-green text-xs font-medium rounded-full">
                Default
              </span>
            )}
            <span className="px-2 py-0.5 bg-gray-100 text-gray-600 text-xs font-medium rounded-full">
              {getTypeLabel(selectedAddress.type)}
            </span>
          </div>
          <p className="text-neutral-gray text-xs mb-1">
            {selectedAddress.street}
          </p>
          <p className="text-neutral-gray text-xs">
            Barangay {selectedAddress.barangay}, {selectedAddress.city}, {selectedAddress.province} {selectedAddress.zipCode}
          </p>
          <p className="text-neutral-gray text-xs mt-1">
            {selectedAddress.phone}
          </p>
          {selectedAddress.landmark && (
            <p className="text-neutral-gray text-xs mt-1">
              <span className="font-medium">Landmark:</span> {selectedAddress.landmark}
            </p>
          )}
          {selectedAddress.deliveryNotes && (
            <p className="text-neutral-gray text-xs mt-1">
              <span className="font-medium">Instructions:</span> {selectedAddress.deliveryNotes}
            </p>
          )}
        </div>
      )}
    </div>
  );
};

// Desktop Delivery Address Component
const DesktopDeliveryAddress = ({ selectedAddress, addresses, loading, apiError, onOpenModal, onRetry, onSetDefault }) => {
  const getAddressIcon = (type) => {
    switch (type) {
      case "home":
        return <FaHome className="text-primary-green" />;
      case "work":
        return <FaBriefcase className="text-blue-500" />;
      default:
        return <FaMapMarkerAlt className="text-gray-500" />;
    }
  };

  const getTypeLabel = (type) => {
    switch (type) {
      case "home":
        return "Home";
      case "work":
        return "Work";
      default:
        return "Other";
    }
  };

  if (loading) {
    return (
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        className="bg-white rounded-xl border-2 border-gray-200 p-6 mb-6"
      >
        <div className="flex items-center justify-between mb-4">
          <h3 className="text-lg font-bold text-dark-charcoal flex items-center gap-2">
            <FaMapMarkerAlt className="text-primary-green" />
            Delivery Address
          </h3>
          <div className="w-24 h-8 bg-gray-200 rounded-lg animate-pulse"></div>
        </div>
        <div className="space-y-3">
          <div className="h-4 bg-gray-200 rounded w-1/3 animate-pulse"></div>
          <div className="h-3 bg-gray-200 rounded w-2/3 animate-pulse"></div>
          <div className="h-3 bg-gray-200 rounded w-1/2 animate-pulse"></div>
        </div>
      </motion.div>
    );
  }

  if (apiError && addresses.length === 0) {
    return (
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        className="bg-white rounded-xl border-2 border-gray-200 p-6 mb-6"
      >
        <h3 className="text-lg font-bold text-dark-charcoal flex items-center gap-2 mb-4">
          <FaMapMarkerAlt className="text-primary-green" />
          Delivery Address
        </h3>
        <div className="text-center py-6">
          <FaExclamationTriangle className="mx-auto text-yellow-500 text-2xl mb-3" />
          <p className="text-gray-600 mb-4">{apiError}</p>
          <button
            onClick={onRetry}
            className="bg-primary-green text-white px-4 py-2 rounded-lg hover:bg-dark-green transition-all"
          >
            Try Again
          </button>
        </div>
      </motion.div>
    );
  }

  if (addresses.length === 0 && !loading) {
    return (
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        className="bg-white rounded-xl border-2 border-gray-200 p-6 mb-6"
      >
        <h3 className="text-lg font-bold text-dark-charcoal flex items-center gap-2 mb-4">
          <FaMapMarkerAlt className="text-primary-green" />
          Delivery Address
        </h3>
        <div className="text-center py-6">
          <FaMapMarkerAlt className="mx-auto text-gray-400 text-3xl mb-3" />
          <h4 className="font-semibold text-dark-charcoal mb-2">
            No delivery addresses
          </h4>
          <p className="text-gray-500 mb-4">
            Please add a delivery address to continue with checkout
          </p>
          <Link
            to="/profile/addresses"
            className="inline-block bg-primary-green text-white px-6 py-3 rounded-lg hover:bg-dark-green transition-all"
          >
            Add Address
          </Link>
        </div>
      </motion.div>
    );
  }

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      className="bg-white rounded-xl border-2 border-gray-200 p-6 mb-6"
    >
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-lg font-bold text-dark-charcoal flex items-center gap-2">
          <FaMapMarkerAlt className="text-primary-green" />
          Delivery Address
        </h3>
        <motion.button
          onClick={onOpenModal}
          whileHover={{ scale: 1.05 }}
          whileTap={{ scale: 0.95 }}
          className="flex items-center gap-2 px-3 py-2 text-primary-green border-2 border-primary-green rounded-lg font-medium text-sm hover:bg-primary-green/5 transition-all"
        >
          <FaEdit className="text-xs" />
          Change
        </motion.button>
      </div>

      {selectedAddress && (
        <div>
          <div className="flex items-center gap-2 mb-1">
            <p className="font-semibold text-dark-charcoal">
              {selectedAddress.name}
            </p>
            {selectedAddress.isDefault && (
              <span className="px-2 py-1 bg-primary-green/10 text-primary-green text-xs font-medium rounded-full">
                Default
              </span>
            )}
            <span className="px-2 py-1 bg-gray-100 text-gray-600 text-xs font-medium rounded-full">
              {getTypeLabel(selectedAddress.type)}
            </span>
          </div>
          <p className="text-neutral-gray text-sm mb-1">
            {selectedAddress.street}
          </p>
          <p className="text-neutral-gray text-sm">
            Barangay {selectedAddress.barangay}, {selectedAddress.city}, {selectedAddress.province} {selectedAddress.zipCode}
          </p>
          <p className="text-neutral-gray text-sm mt-2">
            {selectedAddress.phone}
          </p>
          {selectedAddress.landmark && (
            <p className="text-neutral-gray text-sm">
              <span className="font-medium">Landmark:</span> {selectedAddress.landmark}
            </p>
          )}
          {selectedAddress.deliveryNotes && (
            <p className="text-neutral-gray text-sm">
              <span className="font-medium">Instructions:</span> {selectedAddress.deliveryNotes}
            </p>
          )}
        </div>
      )}
    </motion.div>
  );
};

// Combined Delivery Address Component
const DeliveryAddress = ({ selectedAddress, onAddressChange }) => {
  const [showAddressModal, setShowAddressModal] = useState(false);
  const [addresses, setAddresses] = useState([]);
  const [loading, setLoading] = useState(true);
  const [apiError, setApiError] = useState(null);
  const { success: showSuccess, error: showError } = useAlert();

  useEffect(() => {
    loadAddresses();
  }, []);

  const loadAddresses = async () => {
    setLoading(true);
    setApiError(null);
    try {
      const result = await addressService.getAddresses();

      if (result.success) {
        const userAddresses = result.data || [];
        setAddresses(userAddresses);

        const defaultAddress = userAddresses.find(addr => addr.isDefault) || userAddresses[0];
        if (defaultAddress) {
          onAddressChange(defaultAddress);
        }
      } else {
        setApiError(result.error || "Failed to load addresses");
        showError(result.error || "Failed to load addresses");
      }
    } catch (err) {
      const errorMsg = err.message || "Network error while loading addresses";
      setApiError(errorMsg);
      showError(errorMsg);
    } finally {
      setLoading(false);
    }
  };

  const handleAddressSelect = (address) => {
    onAddressChange(address);
    setShowAddressModal(false);
    showSuccess("Delivery address updated");
  };

  const setSelectedAddress = (address) => {
    onAddressChange(address);
  };

  const handleSetDefault = async (addressId) => {
    try {
      const result = await addressService.setDefaultAddress(addressId);
      if (result.success) {
        showSuccess("Default address updated");
        await loadAddresses();
      } else {
        showError(result.error || "Failed to set default address");
      }
    } catch (err) {
      showError("Failed to set default address");
    }
  };

  const getAddressIcon = (type) => {
    switch (type) {
      case "home":
        return <FaHome className="text-primary-green" />;
      case "work":
        return <FaBriefcase className="text-blue-500" />;
      default:
        return <FaMapMarkerAlt className="text-gray-500" />;
    }
  };

  const getTypeLabel = (type) => {
    switch (type) {
      case "home":
        return "Home";
      case "work":
        return "Work";
      default:
        return "Other";
    }
  };

  return (
    <>
      <div className="block lg:hidden">
        <MobileDeliveryAddress
          selectedAddress={selectedAddress}
          addresses={addresses}
          loading={loading}
          apiError={apiError}
          onOpenModal={() => setShowAddressModal(true)}
          onRetry={loadAddresses}
          onSetDefault={handleSetDefault}
        />
      </div>

      <div className="hidden lg:block">
        <DesktopDeliveryAddress
          selectedAddress={selectedAddress}
          addresses={addresses}
          loading={loading}
          apiError={apiError}
          onOpenModal={() => setShowAddressModal(true)}
          onRetry={loadAddresses}
          onSetDefault={handleSetDefault}
        />
      </div>

      {/* Address Selection Modal */}
      <AnimatePresence>
        {showAddressModal && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4"
            onClick={() => setShowAddressModal(false)}
          >
            <motion.div
              initial={{ opacity: 0, scale: 0.9, y: 20 }}
              animate={{ opacity: 1, scale: 1, y: 0 }}
              exit={{ opacity: 0, scale: 0.9, y: 20 }}
              onClick={(e) => e.stopPropagation()}
              className="bg-white rounded-2xl shadow-2xl max-w-md w-full max-h-[80vh] overflow-hidden"
            >
              <div className="p-6 border-b border-gray-200">
                <h3 className="text-xl font-bold text-dark-charcoal flex items-center gap-2">
                  <FaMapMarkerAlt className="text-primary-green" />
                  Select Delivery Address
                </h3>
                <p className="text-sm text-gray-600 mt-1">
                  {addresses.length} address{addresses.length !== 1 ? 'es' : ''} available
                </p>
              </div>

              <div className="p-4 overflow-y-auto max-h-96">
                <div className="space-y-3">
                  {addresses.map((address) => (
                    <motion.div
                      key={address._id}
                      whileHover={{ scale: 1.02 }}
                      onClick={() => handleAddressSelect(address)}
                      className={`p-4 border-2 rounded-xl cursor-pointer transition-all ${selectedAddress?._id === address._id
                          ? "border-primary-green bg-primary-green/5"
                          : "border-gray-200 hover:border-primary-green/30"
                        }`}
                    >
                      <div className="flex items-start justify-between">
                        <div className="flex-1">
                          <div className="flex items-center gap-2 mb-1">
                            {getAddressIcon(address.type)}
                            <p className="font-semibold text-dark-charcoal">
                              {address.title}
                            </p>
                            {address.isDefault && (
                              <span className="px-2 py-1 bg-primary-green/10 text-primary-green text-xs font-medium rounded-full">
                                Default
                              </span>
                            )}
                            <span className="px-2 py-1 bg-gray-100 text-gray-600 text-xs font-medium rounded-full">
                              {getTypeLabel(address.type)}
                            </span>
                          </div>
                          <p className="text-neutral-gray text-sm mb-1">
                            {address.name}
                          </p>
                          <p className="text-neutral-gray text-sm">
                            {address.street}
                          </p>
                          <p className="text-neutral-gray text-sm">
                            Barangay {address.barangay}, {address.city}
                          </p>
                          <p className="text-neutral-gray text-sm mt-2">
                            {address.phone}
                          </p>
                        </div>
                        {selectedAddress?._id === address._id && (
                          <FaCheck className="text-primary-green mt-1" />
                        )}
                      </div>

                      <div className="flex items-center gap-2 mt-3 pt-3 border-t border-gray-100">
                        {!address.isDefault && (
                          <button
                            onClick={(e) => {
                              e.stopPropagation();
                              handleSetDefault(address._id);
                            }}
                            className="text-xs text-primary-green hover:text-dark-green transition-colors"
                          >
                            Set as default
                          </button>
                        )}
                        <Link
                          to="/profile/addresses"
                          onClick={(e) => e.stopPropagation()}
                          className="text-xs text-blue-500 hover:text-blue-700 transition-colors ml-auto"
                        >
                          Manage addresses
                        </Link>
                      </div>
                    </motion.div>
                  ))}
                </div>
              </div>

              <div className="p-4 border-t border-gray-200">
                <Link
                  to="/profile/addresses"
                  onClick={() => setShowAddressModal(false)}
                >
                  <motion.button
                    whileHover={{ scale: 1.02 }}
                    whileTap={{ scale: 0.98 }}
                    className="w-full py-3 bg-gradient-to-r from-primary-green to-dark-green text-white rounded-xl font-semibold hover:shadow-lg transition-all"
                  >
                    Add New Address
                  </motion.button>
                </Link>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
};

// Mobile Products Ordered
const MobileProductsOrdered = ({ selectedItems }) => (
  <div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
    <h3 className="text-base font-bold text-dark-charcoal flex items-center gap-2 mb-3">
      <FaTruck className="text-primary-green text-sm" />
      Products Ordered ({selectedItems.length})
    </h3>

    <div className="space-y-3">
      {selectedItems.map((item) => (
        <div
          key={item._id}
          className="flex gap-3 p-3 border border-gray-100 rounded-lg"
        >
          {/* Image - Fixed width */}
          <div className="flex-shrink-0 w-20 h-20">
            <img
              src={formatImageUrl(item.product.imageUrl)}
              alt={item.product.name}
              className="w-full h-full object-cover rounded-lg border border-gray-100"
              onError={(e) => {
                e.target.src = "/images/product-placeholder.jpg";
              }}
            />
          </div>

          {/* Product Info - Flexible */}
          <div className="flex-1 min-w-0 flex flex-col justify-between">
            <div>
              <h4 className="font-semibold text-dark-charcoal text-sm line-clamp-2 mb-0.5">
                {item.product.name}
              </h4>
              <p className="text-neutral-gray text-xs">
                {item.product.category}
              </p>
            </div>
            
            {/* Price and Quantity Row */}
            <div className="flex items-end justify-between mt-2">
              <div className="flex items-center gap-1.5">
                <span className="text-neutral-gray text-xs">Qty:</span>
                <span className="text-dark-charcoal text-sm font-semibold">
                  {item.quantity}
                </span>
              </div>
              
              <div className="text-right">
                <p className="font-bold text-dark-charcoal text-sm">
                  ₱{(Number(item.product.price) * item.quantity).toFixed(2)}
                </p>
                <p className="text-primary-green text-xs">
                  ₱{Number(item.product.price).toFixed(2)} each
                </p>
              </div>
            </div>
          </div>
        </div>
      ))}
    </div>
  </div>
);

// Desktop Products Ordered
const DesktopProductsOrdered = ({ selectedItems }) => (
  <motion.div
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.1 }}
    className="bg-white rounded-xl border-2 border-gray-200 p-6 mb-6"
  >
    <h3 className="text-lg font-bold text-dark-charcoal flex items-center gap-2 mb-4">
      <FaTruck className="text-primary-green" />
      Products Ordered ({selectedItems.length})
    </h3>

    <div className="space-y-4">
      {selectedItems.map((item) => (
        <motion.div
          key={item._id}
          initial={{ opacity: 0, x: -20 }}
          animate={{ opacity: 1, x: 0 }}
          className="flex gap-4 p-4 border-2 border-gray-100 rounded-xl hover:border-gray-200 transition-all"
        >
          {/* Image - Fixed size */}
          <div className="flex-shrink-0 w-24 h-24">
            <img
              src={formatImageUrl(item.product.imageUrl)}
              alt={item.product.name}
              className="w-full h-full object-cover rounded-lg border-2 border-gray-100"
              onError={(e) => {
                e.target.src = "/images/product-placeholder.jpg";
              }}
            />
          </div>

          {/* Product Info - Flexible */}
          <div className="flex-1 min-w-0">
            <h4 className="font-semibold text-dark-charcoal text-base mb-1 line-clamp-2">
              {item.product.name}
            </h4>
            <p className="text-neutral-gray text-sm mb-3">
              {item.product.category}
            </p>
            
            <div className="flex items-center gap-2 text-sm">
              <span className="text-neutral-gray">Quantity:</span>
              <span className="text-dark-charcoal font-semibold">
                {item.quantity}
              </span>
              <span className="text-neutral-gray mx-2">•</span>
              <span className="text-primary-green font-medium">
                ₱{Number(item.product.price).toFixed(2)} each
              </span>
            </div>
          </div>

          {/* Price - Fixed width */}
          <div className="flex-shrink-0 text-right flex flex-col justify-center min-w-[100px]">
            <p className="text-sm text-neutral-gray mb-1">Subtotal</p>
            <p className="font-bold text-dark-charcoal text-lg">
              ₱{(Number(item.product.price) * item.quantity).toFixed(2)}
            </p>
          </div>
        </motion.div>
      ))}
    </div>
  </motion.div>
);

const ProductsOrdered = () => {
  const { items = [] } = useCart();
  const selectedItems = items.filter((item) => item.selected);

  return (
    <>
      <div className="block lg:hidden">
        <MobileProductsOrdered selectedItems={selectedItems} />
      </div>
      <div className="hidden lg:block">
        <DesktopProductsOrdered selectedItems={selectedItems} />
      </div>
    </>
  );
};

// Mobile Payment Method
const MobilePaymentMethod = ({ selectedMethod, showPaymentDropdown, paymentMethods, onToggleDropdown, onSelectMethod }) => (
  <div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
    <h3 className="text-base font-bold text-dark-charcoal flex items-center gap-2 mb-3">
      <FaCreditCard className="text-primary-green text-sm" />
      Payment Method
    </h3>

    <div className="relative">
      <button
        onClick={onToggleDropdown}
        className="w-full p-3 border border-gray-200 rounded-lg flex items-center justify-between hover:border-primary-green/30 transition-all"
      >
        <div className="flex items-center gap-2">
          <selectedMethod.icon className="text-primary-green text-base" />
          <div className="text-left">
            <p className="font-semibold text-dark-charcoal text-xs">
              {selectedMethod.name}
            </p>
            <p className="text-neutral-gray text-xs">
              {selectedMethod.description}
            </p>
          </div>
        </div>
        <motion.div
          animate={{ rotate: showPaymentDropdown ? 180 : 0 }}
          transition={{ duration: 0.2 }}
        >
          <FaChevronDown className="text-neutral-gray text-sm" />
        </motion.div>
      </button>

      <AnimatePresence>
        {showPaymentDropdown && (
          <motion.div
            initial={{ opacity: 0, y: -10, height: 0 }}
            animate={{ opacity: 1, y: 0, height: "auto" }}
            exit={{ opacity: 0, y: -10, height: 0 }}
            className="absolute top-full left-0 right-0 mt-2 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden z-10"
          >
            <div className="p-2">
              {paymentMethods.map((method) => (
                <button
                  key={method.id}
                  onClick={() => onSelectMethod(method)}
                  className={`w-full p-3 rounded-lg flex items-center gap-2 text-left transition-all ${selectedMethod.id === method.id
                      ? "bg-primary-green/10"
                      : "hover:bg-gray-50"
                    }`}
                >
                  <method.icon
                    className={`text-base ${selectedMethod.id === method.id
                        ? "text-primary-green"
                        : "text-neutral-gray"
                      }`}
                  />
                  <div className="flex-1">
                    <p
                      className={`font-semibold text-xs ${selectedMethod.id === method.id
                          ? "text-primary-green"
                          : "text-dark-charcoal"
                        }`}
                    >
                      {method.name}
                    </p>
                    <p className="text-neutral-gray text-xs">
                      {method.description}
                    </p>
                  </div>
                  {selectedMethod.id === method.id && (
                    <FaCheck className="text-primary-green text-sm" />
                  )}
                </button>
              ))}
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>

    <div className="mt-3 p-3 bg-gray-50 rounded-lg">
      <div className="flex items-center gap-2 text-xs text-neutral-gray mb-1">
        <FaShieldAlt className="text-primary-green" />
        <span className="font-medium">Secure Payment</span>
      </div>
      <p className="text-xs text-neutral-gray">
        Your payment information is encrypted and secure.
      </p>
    </div>
  </div>
);

// Desktop Payment Method
const DesktopPaymentMethod = ({ selectedMethod, showPaymentDropdown, paymentMethods, onToggleDropdown, onSelectMethod }) => (
  <motion.div
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.2 }}
    className="bg-white rounded-xl border-2 border-gray-200 p-6 mb-6"
  >
    <h3 className="text-lg font-bold text-dark-charcoal flex items-center gap-2 mb-4">
      <FaCreditCard className="text-primary-green" />
      Payment Method
    </h3>

    <div className="relative">
      <motion.button
        onClick={onToggleDropdown}
        whileHover={{ scale: 1.02 }}
        whileTap={{ scale: 0.98 }}
        className="w-full p-4 border-2 border-gray-200 rounded-xl flex items-center justify-between hover:border-primary-green/30 transition-all"
      >
        <div className="flex items-center gap-3">
          <selectedMethod.icon className="text-primary-green text-lg" />
          <div className="text-left">
            <p className="font-semibold text-dark-charcoal text-sm">
              {selectedMethod.name}
            </p>
            <p className="text-neutral-gray text-xs">
              {selectedMethod.description}
            </p>
          </div>
        </div>
        <motion.div
          animate={{ rotate: showPaymentDropdown ? 180 : 0 }}
          transition={{ duration: 0.2 }}
        >
          <FaChevronDown className="text-neutral-gray" />
        </motion.div>
      </motion.button>

      <AnimatePresence>
        {showPaymentDropdown && (
          <motion.div
            initial={{ opacity: 0, y: -10, height: 0 }}
            animate={{ opacity: 1, y: 0, height: "auto" }}
            exit={{ opacity: 0, y: -10, height: 0 }}
            className="absolute top-full left-0 right-0 mt-2 bg-white border-2 border-gray-200 rounded-xl shadow-lg overflow-hidden z-10"
          >
            <div className="p-2">
              {paymentMethods.map((method) => (
                <motion.button
                  key={method.id}
                  onClick={() => onSelectMethod(method)}
                  whileHover={{ backgroundColor: "rgba(34, 197, 94, 0.05)" }}
                  className={`w-full p-3 rounded-lg flex items-center gap-3 text-left transition-all ${selectedMethod.id === method.id
                      ? "bg-primary-green/10"
                      : "hover:bg-gray-50"
                    }`}
                >
                  <method.icon
                    className={`text-lg ${selectedMethod.id === method.id
                        ? "text-primary-green"
                        : "text-neutral-gray"
                      }`}
                  />
                  <div className="flex-1">
                    <p
                      className={`font-semibold text-sm ${selectedMethod.id === method.id
                          ? "text-primary-green"
                          : "text-dark-charcoal"
                        }`}
                    >
                      {method.name}
                    </p>
                    <p className="text-neutral-gray text-xs">
                      {method.description}
                    </p>
                  </div>
                  {selectedMethod.id === method.id && (
                    <FaCheck className="text-primary-green" />
                  )}
                </motion.button>
              ))}
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>

    <div className="mt-4 p-4 bg-gray-50 rounded-lg">
      <div className="flex items-center gap-2 text-sm text-neutral-gray mb-2">
        <FaShieldAlt className="text-primary-green" />
        <span className="font-medium">Secure Payment</span>
      </div>
      <p className="text-xs text-neutral-gray">
        Your payment information is encrypted and secure.
      </p>
    </div>
  </motion.div>
);

const PaymentMethod = () => {
  const [showPaymentDropdown, setShowPaymentDropdown] = useState(false);
  const [selectedMethod, setSelectedMethod] = useState({
    id: "online",
    name: "Online Payment",
    description: "Pay securely with your card or e-wallet",
    icon: FaCreditCard,
  });

  const paymentMethods = [
    {
      id: "online",
      name: "Online Payment",
      description: "Pay securely with your card or e-wallet",
      icon: FaCreditCard,
    },
  ];

  const handleSelectMethod = (method) => {
    setSelectedMethod(method);
    setShowPaymentDropdown(false);
  };

  return (
    <>
      <div className="block lg:hidden">
        <MobilePaymentMethod
          selectedMethod={selectedMethod}
          showPaymentDropdown={showPaymentDropdown}
          paymentMethods={paymentMethods}
          onToggleDropdown={() => setShowPaymentDropdown(!showPaymentDropdown)}
          onSelectMethod={handleSelectMethod}
        />
      </div>
      <div className="hidden lg:block">
        <DesktopPaymentMethod
          selectedMethod={selectedMethod}
          showPaymentDropdown={showPaymentDropdown}
          paymentMethods={paymentMethods}
          onToggleDropdown={() => setShowPaymentDropdown(!showPaymentDropdown)}
          onSelectMethod={handleSelectMethod}
        />
      </div>
    </>
  );
};

// Mobile Order Summary
const MobileOrderSummary = ({ selectedItems, subtotal, shippingFee, total, isProcessing, onPlaceOrder }) => (
  <div className="bg-white rounded-lg border border-gray-200 p-4 shadow-lg">
    <h3 className="text-base font-bold text-dark-charcoal mb-3 flex items-center gap-2">
      <FaCheck className="text-primary-green text-sm" />
      Order Summary
    </h3>

    <div className="space-y-2 mb-3">
      <div className="flex justify-between text-xs">
        <span className="text-neutral-gray">Subtotal ({selectedItems.length} items):</span>
        <span className="font-medium text-dark-charcoal">
          ₱{subtotal.toFixed(2)}
        </span>
      </div>

      <div className="flex justify-between text-xs">
        <span className="text-neutral-gray">Shipping Fee:</span>
        <span className="font-medium text-dark-charcoal">
          ₱{shippingFee.toFixed(2)}
        </span>
      </div>

      <div className="border-t border-gray-200 pt-2">
        <div className="flex justify-between items-center">
          <span className="font-semibold text-dark-charcoal text-sm">Total:</span>
          <span className="text-lg font-bold text-primary-green">
            ₱{total.toFixed(2)}
          </span>
        </div>
      </div>
    </div>

    <div className="flex items-center justify-center gap-2 py-2 border-t border-gray-200 mb-3">
      <FaLock className="text-primary-green text-xs" />
      <span className="text-xs text-neutral-gray">Secure SSL Encryption</span>
    </div>

    <button
      onClick={onPlaceOrder}
      disabled={isProcessing}
      className="w-full py-3 bg-gradient-to-r from-primary-green to-dark-green text-white rounded-lg font-bold shadow-lg hover:shadow-xl transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
    >
      <FaLock className="text-xs" />
      {isProcessing ? "Processing..." : "Place Order"}
    </button>

    <p className="text-xs text-center text-neutral-gray mt-2">
      By placing your order, you agree to our Terms of Service
    </p>
  </div>
);

// Desktop Order Summary
const DesktopOrderSummary = ({ selectedItems, subtotal, shippingFee, total, isProcessing, onPlaceOrder }) => (
  <motion.div
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.3 }}
    className="sticky top-4 bg-white rounded-xl border-2 border-gray-200 p-6 shadow-lg"
  >
    <h3 className="text-lg font-bold text-dark-charcoal mb-4 flex items-center gap-2">
      <FaCheck className="text-primary-green" />
      Order Summary
    </h3>

    <div className="space-y-3 mb-4">
      <div className="flex justify-between text-sm">
        <span className="text-neutral-gray">Subtotal ({selectedItems.length} items):</span>
        <span className="font-medium text-dark-charcoal">
          ₱{subtotal.toFixed(2)}
        </span>
      </div>

      <div className="flex justify-between text-sm">
        <span className="text-neutral-gray">Shipping Fee:</span>
        <span className="font-medium text-dark-charcoal">
          ₱{shippingFee.toFixed(2)}
        </span>
      </div>

      <div className="border-t border-gray-200 pt-3">
        <div className="flex justify-between items-center">
          <span className="font-semibold text-dark-charcoal">Total:</span>
          <span className="text-xl font-bold text-primary-green">
            ₱{total.toFixed(2)}
          </span>
        </div>
      </div>
    </div>

    <div className="flex items-center justify-center gap-2 py-3 border-t border-gray-200 mb-4">
      <FaLock className="text-primary-green text-sm" />
      <span className="text-xs text-neutral-gray">Secure SSL Encryption</span>
    </div>

    <motion.button
      onClick={onPlaceOrder}
      disabled={isProcessing}
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.98 }}
      className="w-full py-4 bg-gradient-to-r from-primary-green to-dark-green text-white rounded-xl font-bold shadow-lg hover:shadow-xl transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
    >
      <FaLock className="text-sm" />
      {isProcessing ? "Processing..." : "Place Order"}
    </motion.button>

    <p className="text-xs text-center text-neutral-gray mt-3">
      By placing your order, you agree to our Terms of Service
    </p>
  </motion.div>
);

const OrderSummary = ({ selectedAddress }) => {
  const { items = [], removeItem, refresh } = useCart(); // ✅ ADD removeItem and refresh
  const navigate = useNavigate();
  const { success, error } = useAlert();
  const { user } = useAuth();
  const [isProcessing, setIsProcessing] = useState(false);

  const selectedItems = items.filter((item) => item.selected);
  const subtotal = selectedItems.reduce(
    (sum, item) => sum + item.product.price * item.quantity,
    0
  );
  const shippingFee = 50;
  const total = subtotal + shippingFee;

  const handlePlaceOrder = async () => {
    if (isProcessing) return;

    // ✅ BLOCK: Cannot place order without address
    if (!selectedAddress) {
      error("Please select a delivery address before placing your order");
      return;
    }

    setIsProcessing(true);

    try {
      const normalizedItems = selectedItems.map(item => {
        // ✅ FIX: Use actual quantity from item, prioritize quantity over qty
        // Ensure we're using the actual selected quantity, not a default
        let actualQuantity = item.quantity;
        if (actualQuantity === undefined || actualQuantity === null) {
          actualQuantity = item.qty;
        }
        actualQuantity = Number(actualQuantity);

        // Validate quantity is a positive number
        if (isNaN(actualQuantity) || actualQuantity < 1) {
          console.warn(`Invalid quantity for item ${item._id}: ${item.quantity || item.qty}, using 1`);
          actualQuantity = 1;
        }

        return {
          productId: item.product._id || item.productId || item.product?._id,
          qty: actualQuantity, // Use the validated actual quantity
          cartItemId: item._id // ✅ ADD cart item ID for removal
        };
      });

      // Get selected address ID from props
      const selectedAddressId = selectedAddress?._id;

      const result = await processOnlinePayment(normalizedItems, {
        customerEmail: user?.email,
        addressId: selectedAddressId,
        onSuccess: async ({ orderId, invoiceUrl }) => { // ✅ MAKE ASYNC
          success(`Order #${orderId} created! Redirecting to payment...`);

          // ✅ REMOVE PURCHASED ITEMS FROM CART
          try {
            // Remove each selected item from cart
            for (const item of selectedItems) {
              await removeItem(item._id);
            }
            // Refresh cart to ensure state is synchronized
            await refresh();
          } catch (removeError) {
            console.warn("Failed to remove items from cart:", removeError);
            // Continue with payment even if cart cleanup fails
          }

          setTimeout(() => redirectToPayment(invoiceUrl), 1000);
        },
        onError: (errMsg) => {
          error(`Payment failed: ${errMsg}`);
        }
      });

      if (!result.success) {
        error(result.error || "Payment could not be processed.");
      }
    } catch (err) {
      console.error("Checkout Error:", err);
      error("Unexpected error during checkout. Please try again.");
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <>
      <div className="block lg:hidden">
        <MobileOrderSummary
          selectedItems={selectedItems}
          subtotal={subtotal}
          shippingFee={shippingFee}
          total={total}
          isProcessing={isProcessing}
          onPlaceOrder={handlePlaceOrder}
        />
      </div>
      <div className="hidden lg:block">
        <DesktopOrderSummary
          selectedItems={selectedItems}
          subtotal={subtotal}
          shippingFee={shippingFee}
          total={total}
          isProcessing={isProcessing}
          onPlaceOrder={handlePlaceOrder}
        />
      </div>
    </>
  );
};

// Main Checkout Page Component
export const CheckoutPage = () => {
  const { items = [] } = useCart();
  const navigate = useNavigate();
  const [selectedAddress, setSelectedAddress] = useState(null);
  const selectedItems = items.filter((item) => item.selected);

  if (selectedItems.length === 0) {
    return (
      <div className="min-h-screen bg-gradient-to-br from-off-white via-white to-off-white py-8 flex items-center justify-center">
        <div className="max-w-md mx-auto px-4 text-center">
          <div className="bg-white rounded-2xl shadow-xl p-8">
            <div className="w-16 h-16 bg-primary-green/10 rounded-full flex items-center justify-center mx-auto mb-4">
              <FaCreditCard className="text-primary-green text-2xl" />
            </div>
            <h2 className="text-xl font-bold text-dark-charcoal mb-2">
              No Items Selected
            </h2>
            <p className="text-neutral-gray text-sm mb-6">
              Please select items from your cart to proceed with checkout.
            </p>
            <Link
              to="/cart"
              className="inline-block px-6 py-3 bg-gradient-to-r from-primary-green to-dark-green text-white rounded-xl font-semibold hover:shadow-lg transition-all"
            >
              Back to Cart
            </Link>
          </div>
        </div>
      </div>
    );
  }

  return (
    <>
      {/* Mobile View */}
      <div className="block lg:hidden min-h-screen bg-gradient-to-br from-off-white via-white to-off-white py-4">
        <div className="max-w-7xl mx-auto px-4">
          {/* Mobile Header */}
          <div className="mb-4">
            <div className="flex items-center gap-3 mb-2">
              <button
                onClick={() => navigate(-1)}
                className="p-2 hover:bg-gray-100 rounded-lg transition-all"
                aria-label="Go back"
              >
                <FaArrowLeft className="text-gray-600" />
              </button>

              <h1 className="text-lg font-bold text-dark-charcoal flex items-center gap-2">
                <FaLock className="text-primary-green text-base" />
                Checkout
              </h1>
            </div>
            <p className="text-neutral-gray text-xs ml-11">
              Complete your purchase securely
            </p>
          </div>

          {/* Mobile Content */}
          <div className="space-y-4">
            <DeliveryAddress selectedAddress={selectedAddress} onAddressChange={setSelectedAddress} />
            <ProductsOrdered />
            <PaymentMethod />
            <OrderSummary selectedAddress={selectedAddress} />
          </div>
        </div>
      </div>

      {/* Desktop View */}
      <div className="hidden lg:block min-h-screen bg-gradient-to-br from-off-white via-white to-off-white py-8">
        <div className="max-w-7xl mx-auto px-8">
          {/* Desktop Header */}
          <div className="mb-8">
            <div className="flex items-center gap-4 mb-2">
              <motion.button
                onClick={() => navigate(-1)}
                whileHover={{ scale: 1.05 }}
                whileTap={{ scale: 0.95 }}
                className="p-2 hover:bg-gray-100 rounded-lg transition-all"
                aria-label="Go back to cart"
              >
                <FaArrowLeft className="text-gray-600" />
              </motion.button>

              <h1 className="text-3xl font-bold text-dark-charcoal flex items-center gap-2">
                <FaLock className="text-primary-green" />
                Checkout
              </h1>
            </div>
            <p className="text-neutral-gray text-sm ml-14">
              Complete your purchase securely
            </p>
          </div>

          {/* Desktop Content Grid */}
          <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
            <div className="lg:col-span-2 space-y-4">
              <DeliveryAddress selectedAddress={selectedAddress} onAddressChange={setSelectedAddress} />
              <ProductsOrdered />
              <PaymentMethod />
            </div>

            <div className="lg:col-span-1">
              <OrderSummary selectedAddress={selectedAddress} />
            </div>
          </div>
        </div>
      </div>
    </>
  );
};

CheckoutPage.displayName = "CheckoutPage";
Editor is loading...
Leave a Comment