feat: implement header/footer visibility controls for checkout and thankyou pages
- Created LayoutWrapper component to conditionally render header/footer based on route - Created MinimalHeader component (logo only) - Created MinimalFooter component (trust badges + policy links) - Created usePageVisibility hook to get visibility settings per page - Wrapped ClassicLayout with LayoutWrapper for conditional rendering - Header/footer visibility now controlled directly in React SPA - Settings: show/minimal/hide for both header and footer - Background color support for checkout and thankyou pages
This commit is contained in:
@@ -1,20 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCartStore } from '@/lib/cart/store';
|
||||
import { useCheckoutSettings } from '@/hooks/useAppearanceSettings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Container from '@/components/Layout/Container';
|
||||
import { formatPrice } from '@/lib/currency';
|
||||
import { ArrowLeft, ShoppingBag } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
|
||||
export default function Checkout() {
|
||||
const navigate = useNavigate();
|
||||
const { cart } = useCartStore();
|
||||
const { layout, elements } = useCheckoutSettings();
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const user = (window as any).woonoowCustomer?.user;
|
||||
|
||||
// Check if cart contains only virtual/downloadable products
|
||||
const isVirtualOnly = React.useMemo(() => {
|
||||
if (cart.items.length === 0) return false;
|
||||
return cart.items.every(item => item.virtual || item.downloadable);
|
||||
}, [cart.items]);
|
||||
|
||||
// Calculate totals
|
||||
const subtotal = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const shipping = 0; // TODO: Calculate shipping
|
||||
const shipping = isVirtualOnly ? 0 : 0; // No shipping for virtual products
|
||||
const tax = 0; // TODO: Calculate tax
|
||||
const total = subtotal + shipping + tax;
|
||||
|
||||
@@ -43,20 +53,98 @@ export default function Checkout() {
|
||||
|
||||
const [shipToDifferentAddress, setShipToDifferentAddress] = useState(false);
|
||||
const [orderNotes, setOrderNotes] = useState('');
|
||||
const [paymentMethod, setPaymentMethod] = useState(isVirtualOnly ? 'bacs' : 'cod');
|
||||
|
||||
// Auto-fill form with user data if logged in
|
||||
useEffect(() => {
|
||||
if (user?.isLoggedIn && user?.billing) {
|
||||
setBillingData({
|
||||
firstName: user.billing.first_name || '',
|
||||
lastName: user.billing.last_name || '',
|
||||
email: user.billing.email || user.email || '',
|
||||
phone: user.billing.phone || '',
|
||||
address: user.billing.address_1 || '',
|
||||
city: user.billing.city || '',
|
||||
state: user.billing.state || '',
|
||||
postcode: user.billing.postcode || '',
|
||||
country: user.billing.country || '',
|
||||
});
|
||||
}
|
||||
|
||||
if (user?.isLoggedIn && user?.shipping) {
|
||||
setShippingData({
|
||||
firstName: user.shipping.first_name || '',
|
||||
lastName: user.shipping.last_name || '',
|
||||
address: user.shipping.address_1 || '',
|
||||
city: user.shipping.city || '',
|
||||
state: user.shipping.state || '',
|
||||
postcode: user.shipping.postcode || '',
|
||||
country: user.shipping.country || '',
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handlePlaceOrder = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
// TODO: Implement order placement API call
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
|
||||
// Prepare order data
|
||||
const orderData = {
|
||||
items: cart.items.map(item => ({
|
||||
product_id: item.product_id,
|
||||
variation_id: item.variation_id,
|
||||
qty: item.quantity,
|
||||
meta: item.attributes ? Object.entries(item.attributes).map(([key, value]) => ({
|
||||
key,
|
||||
value
|
||||
})) : []
|
||||
})),
|
||||
billing: {
|
||||
first_name: billingData.firstName,
|
||||
last_name: billingData.lastName,
|
||||
email: billingData.email,
|
||||
phone: billingData.phone,
|
||||
address_1: billingData.address,
|
||||
city: billingData.city,
|
||||
state: billingData.state,
|
||||
postcode: billingData.postcode,
|
||||
country: billingData.country,
|
||||
},
|
||||
shipping: shipToDifferentAddress ? {
|
||||
first_name: shippingData.firstName,
|
||||
last_name: shippingData.lastName,
|
||||
address_1: shippingData.address,
|
||||
city: shippingData.city,
|
||||
state: shippingData.state,
|
||||
postcode: shippingData.postcode,
|
||||
country: shippingData.country,
|
||||
ship_to_different: true,
|
||||
} : {
|
||||
ship_to_different: false,
|
||||
},
|
||||
payment_method: paymentMethod,
|
||||
customer_note: orderNotes,
|
||||
};
|
||||
|
||||
// Submit order
|
||||
const response = await apiClient.post('/checkout/submit', orderData);
|
||||
const data = (response as any).data || response;
|
||||
|
||||
toast.success('Order placed successfully!');
|
||||
navigate('/order-received/123'); // TODO: Use actual order ID
|
||||
} catch (error) {
|
||||
toast.error('Failed to place order');
|
||||
console.error(error);
|
||||
if (data.ok && data.order_id) {
|
||||
// Clear cart
|
||||
cart.items.forEach(item => {
|
||||
useCartStore.getState().removeItem(item.key);
|
||||
});
|
||||
|
||||
toast.success('Order placed successfully!');
|
||||
navigate(`/order-received/${data.order_id}`);
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to create order');
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to place order');
|
||||
console.error('Order creation error:', error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
@@ -92,9 +180,9 @@ export default function Checkout() {
|
||||
</div>
|
||||
|
||||
<form onSubmit={handlePlaceOrder}>
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
<div className={`grid gap-8 ${layout.style === 'single-column' ? 'grid-cols-1' : layout.order_summary === 'top' ? 'grid-cols-1' : 'lg:grid-cols-3'}`}>
|
||||
{/* Billing & Shipping Forms */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className={`space-y-6 ${layout.style === 'single-column' || layout.order_summary === 'top' ? '' : 'lg:col-span-2'}`}>
|
||||
{/* Billing Details */}
|
||||
<div className="bg-white border rounded-lg p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Billing Details</h2>
|
||||
@@ -139,60 +227,67 @@ export default function Checkout() {
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium mb-2">Street Address *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.address}
|
||||
onChange={(e) => setBillingData({ ...billingData, address: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">City *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.city}
|
||||
onChange={(e) => setBillingData({ ...billingData, city: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">State / Province *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.state}
|
||||
onChange={(e) => setBillingData({ ...billingData, state: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.postcode}
|
||||
onChange={(e) => setBillingData({ ...billingData, postcode: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Country *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.country}
|
||||
onChange={(e) => setBillingData({ ...billingData, country: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Address fields - only for physical products */}
|
||||
{!isVirtualOnly && (
|
||||
<>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium mb-2">Street Address *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.address}
|
||||
onChange={(e) => setBillingData({ ...billingData, address: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">City *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.city}
|
||||
onChange={(e) => setBillingData({ ...billingData, city: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">State / Province *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.state}
|
||||
onChange={(e) => setBillingData({ ...billingData, state: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.postcode}
|
||||
onChange={(e) => setBillingData({ ...billingData, postcode: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Country *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingData.country}
|
||||
onChange={(e) => setBillingData({ ...billingData, country: e.target.value })}
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ship to Different Address */}
|
||||
{/* Ship to Different Address - only for physical products */}
|
||||
{!isVirtualOnly && (
|
||||
<div className="bg-white border rounded-lg p-6">
|
||||
<label className="flex items-center gap-2 mb-4">
|
||||
<input
|
||||
@@ -279,24 +374,42 @@ export default function Checkout() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Order Notes */}
|
||||
<div className="bg-white border rounded-lg p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Order Notes (Optional)</h2>
|
||||
<textarea
|
||||
value={orderNotes}
|
||||
onChange={(e) => setOrderNotes(e.target.value)}
|
||||
placeholder="Notes about your order, e.g. special notes for delivery."
|
||||
className="w-full border rounded-lg px-4 py-2 h-32"
|
||||
/>
|
||||
</div>
|
||||
{elements.order_notes && (
|
||||
<div className="bg-white border rounded-lg p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Order Notes (Optional)</h2>
|
||||
<textarea
|
||||
value={orderNotes}
|
||||
onChange={(e) => setOrderNotes(e.target.value)}
|
||||
placeholder="Notes about your order, e.g. special notes for delivery."
|
||||
className="w-full border rounded-lg px-4 py-2 h-32"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Order Summary */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className={`${layout.style === 'single-column' || layout.order_summary === 'top' ? 'order-first' : 'lg:col-span-1'}`}>
|
||||
<div className="bg-white border rounded-lg p-6 sticky top-4">
|
||||
<h2 className="text-xl font-bold mb-4">Your Order</h2>
|
||||
|
||||
{/* Coupon Field */}
|
||||
{elements.coupon_field && (
|
||||
<div className="mb-4 pb-4 border-b">
|
||||
<label className="block text-sm font-medium mb-2">Coupon Code</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter coupon code"
|
||||
className="flex-1 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm">Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Order Items */}
|
||||
<div className="space-y-3 mb-4 pb-4 border-b">
|
||||
{cart.items.map((item) => (
|
||||
@@ -311,6 +424,29 @@ export default function Checkout() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Shipping Options */}
|
||||
{elements.shipping_options && (
|
||||
<div className="mb-4 pb-4 border-b">
|
||||
<h3 className="font-medium mb-3">Shipping Method</h3>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center justify-between p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="radio" name="shipping" value="free" defaultChecked className="w-4 h-4" />
|
||||
<span className="text-sm">Free Shipping</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">Free</span>
|
||||
</label>
|
||||
<label className="flex items-center justify-between p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="radio" name="shipping" value="express" className="w-4 h-4" />
|
||||
<span className="text-sm">Express Shipping</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">$15.00</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2 mb-6">
|
||||
<div className="flex justify-between text-sm">
|
||||
@@ -337,29 +473,74 @@ export default function Checkout() {
|
||||
<div className="mb-6">
|
||||
<h3 className="font-medium mb-3">Payment Method</h3>
|
||||
<div className="space-y-2">
|
||||
{/* Hide COD for virtual-only products */}
|
||||
{!isVirtualOnly && (
|
||||
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="cod"
|
||||
checked={paymentMethod === 'cod'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span>Cash on Delivery</span>
|
||||
</label>
|
||||
)}
|
||||
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<input type="radio" name="payment" value="cod" defaultChecked className="w-4 h-4" />
|
||||
<span>Cash on Delivery</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
||||
<input type="radio" name="payment" value="bank" className="w-4 h-4" />
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="bacs"
|
||||
checked={paymentMethod === 'bacs'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span>Bank Transfer</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Payment Icons */}
|
||||
{elements.payment_icons && (
|
||||
<div className="flex items-center gap-2 mt-3 pt-3 border-t">
|
||||
<span className="text-xs text-gray-500">We accept:</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-10 h-6 bg-gray-100 rounded flex items-center justify-center text-xs font-bold">VISA</div>
|
||||
<div className="w-10 h-6 bg-gray-100 rounded flex items-center justify-center text-xs font-bold">MC</div>
|
||||
<div className="w-10 h-6 bg-gray-100 rounded flex items-center justify-center text-xs font-bold">AMEX</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Place Order Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Processing...' : 'Place Order'}
|
||||
</Button>
|
||||
{/* Place Order Button - Only show in sidebar layout */}
|
||||
{layout.order_summary !== 'top' && (
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Processing...' : 'Place Order'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Place Order Button - Show at bottom when summary is on top */}
|
||||
{layout.order_summary === 'top' && (
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Processing...' : 'Place Order'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user