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:
Dwindi Ramadhana
2025-12-25 22:20:48 +07:00
parent c37ecb8e96
commit 9ac09582d2
104 changed files with 14801 additions and 1213 deletions

View File

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