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
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