Files
WooNooW/customer-spa/src/pages/ThankYou/index.tsx
Dwindi Ramadhana 10b3c0e47f feat: Go-to-Account button + wishlist merge on login
1. ThankYou page - Go to Account button:
   - Added for logged-in users (next to Continue Shopping)
   - Shows in both receipt and basic templates
   - Uses outline variant with User icon

2. Wishlist merge on login:
   - Reads guest wishlist from localStorage (woonoow_guest_wishlist)
   - POSTs each product to /account/wishlist API
   - Handles duplicates gracefully (skips on error)
   - Clears localStorage after successful merge
2026-01-01 17:17:12 +07:00

498 lines
22 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { useParams, Link, useSearchParams } from 'react-router-dom';
import { useThankYouSettings } from '@/hooks/useAppearanceSettings';
import Container from '@/components/Layout/Container';
import { CheckCircle, ShoppingBag, Package, Truck, User } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { formatPrice } from '@/lib/currency';
import { apiClient } from '@/lib/api/client';
export default function ThankYou() {
const { orderId } = useParams<{ orderId: string }>();
const [searchParams] = useSearchParams();
const orderKey = searchParams.get('key');
const { template, headerVisibility, footerVisibility, backgroundColor, customMessage, elements, isLoading: settingsLoading } = useThankYouSettings();
const [order, setOrder] = useState<any>(null);
const [relatedProducts, setRelatedProducts] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const isLoggedIn = (window as any).woonoowCustomer?.user?.isLoggedIn;
useEffect(() => {
const fetchOrderData = async () => {
if (!orderId) return;
try {
// Use public order endpoint with key validation
const keyParam = orderKey ? `?key=${orderKey}` : '';
const orderData = await apiClient.get(`/checkout/order/${orderId}${keyParam}`) as any;
setOrder(orderData);
// Fetch related products from first order item
if (orderData.items && orderData.items.length > 0) {
const firstProductId = orderData.items[0].product_id;
const productData = await apiClient.get(`/shop/products/${firstProductId}`) as any;
if (productData.related_products && productData.related_products.length > 0) {
setRelatedProducts(productData.related_products.slice(0, 4));
}
}
} catch (err: any) {
console.error('Failed to fetch order data:', err);
setError(err.message || 'Failed to load order');
} finally {
setLoading(false);
}
};
fetchOrderData();
}, [orderId, orderKey]);
if (loading || settingsLoading || !order) {
return (
<Container>
<div className="py-20 text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mx-auto"></div>
</div>
</Container>
);
}
const getStatusLabel = (status: string) => {
const statusMap: Record<string, string> = {
'pending': 'Pending Payment',
'processing': 'Processing',
'on-hold': 'On Hold',
'completed': 'Completed',
'cancelled': 'Cancelled',
'refunded': 'Refunded',
'failed': 'Failed',
};
return statusMap[status] || status.charAt(0).toUpperCase() + status.slice(1);
};
// Render receipt style template
if (template === 'receipt') {
return (
<div style={{ backgroundColor }}>
<Container>
<div className="py-12 max-w-2xl mx-auto">
{/* Receipt Container */}
<div className="bg-white shadow-lg" style={{ fontFamily: 'monospace' }}>
{/* Receipt Header */}
<div className="border-b-2 border-dashed border-gray-400 p-8 text-center">
<div className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-4">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
<h1 className="text-2xl font-bold mb-2">PAYMENT RECEIPT</h1>
<p className="text-gray-600">Order #{order.number}</p>
<p className="text-sm text-gray-500 mt-1">
{new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
{/* Custom Message */}
<div className="px-8 py-4 bg-gray-50 border-b border-dashed border-gray-300">
<p className="text-sm text-center text-gray-700">{customMessage}</p>
</div>
{/* Order Items */}
{elements.order_details && (
<div className="p-8">
<div className="border-b-2 border-gray-900 pb-2 mb-4">
<div className="flex justify-between text-sm font-bold">
<span>ITEM</span>
<span>AMOUNT</span>
</div>
</div>
<div className="space-y-3">
{order.items.map((item: any) => (
<div key={item.id}>
<div className="flex justify-between">
<div className="flex-1">
<div className="font-medium">{item.name}</div>
<div className="text-sm text-gray-600">Qty: {item.qty}</div>
</div>
<div className="text-right font-mono">
{formatPrice(item.total)}
</div>
</div>
</div>
))}
</div>
{/* Totals */}
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2">
<div className="flex justify-between text-sm">
<span>SUBTOTAL:</span>
<span className="font-mono">{formatPrice(parseFloat(order.subtotal || 0))}</span>
</div>
{parseFloat(order.shipping_total || 0) > 0 && (
<div className="flex justify-between text-sm">
<span>SHIPPING:</span>
<span className="font-mono">{formatPrice(parseFloat(order.shipping_total))}</span>
</div>
)}
{parseFloat(order.tax_total || 0) > 0 && (
<div className="flex justify-between text-sm">
<span>TAX:</span>
<span className="font-mono">{formatPrice(parseFloat(order.tax_total))}</span>
</div>
)}
<div className="flex justify-between text-lg font-bold border-t-2 border-gray-900 pt-2 mt-2">
<span>TOTAL:</span>
<span className="font-mono">{formatPrice(parseFloat(order.total || 0))}</span>
</div>
</div>
{/* Payment & Status Info */}
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Payment Method:</span>
<span className="font-medium uppercase">{order.payment_method || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Status:</span>
<span className="font-medium uppercase">{getStatusLabel(order.status)}</span>
</div>
</div>
{/* Customer Info */}
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4">
<div className="text-xs text-gray-600 uppercase mb-2">Bill To:</div>
<div className="text-sm">
<div className="font-medium">
{order.billing?.first_name} {order.billing?.last_name}
</div>
<div className="text-gray-600">{order.billing?.email}</div>
{order.billing?.phone && (
<div className="text-gray-600">{order.billing.phone}</div>
)}
</div>
</div>
</div>
)}
{/* Receipt Footer */}
<div className="border-t-2 border-dashed border-gray-400 p-8 text-center bg-gray-50">
<p className="text-sm text-gray-600 mb-4">
{order.status === 'pending'
? 'Awaiting payment confirmation'
: 'Thank you for your business!'}
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
{elements.continue_shopping_button && (
<Link to="/shop">
<Button size="lg" className="gap-2">
<ShoppingBag className="w-5 h-5" />
Continue Shopping
</Button>
</Link>
)}
{isLoggedIn && (
<Link to="/my-account">
<Button size="lg" variant="outline" className="gap-2">
<User className="w-5 h-5" />
Go to Account
</Button>
</Link>
)}
</div>
</div>
</div>
{/* Related Products */}
{elements.related_products && relatedProducts.length > 0 && (
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{relatedProducts.map((product: any) => (
<Link key={product.id} to={`/product/${product.slug}`} className="group no-underline">
<div className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-square bg-gray-100 flex items-center justify-center">
{product.image ? (
<img src={product.image} alt={product.name} className="w-full !h-full object-cover" />
) : (
<Package className="w-12 h-12 text-gray-400" />
)}
</div>
<div className="p-3">
<h3 className="font-medium text-sm text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2">
{product.name}
</h3>
<p className="text-sm font-bold text-gray-900 mt-1">
{formatPrice(parseFloat(product.price || 0))}
</p>
</div>
</div>
</Link>
))}
</div>
{/* Totals */}
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2">
<div className="flex justify-between text-sm">
<span>SUBTOTAL:</span>
<span className="font-mono">{formatPrice(parseFloat(order.subtotal || 0))}</span>
</div>
{parseFloat(order.shipping_total || 0) > 0 && (
<div className="flex justify-between text-sm">
<span>SHIPPING:</span>
<span className="font-mono">{formatPrice(parseFloat(order.shipping_total))}</span>
</div>
)}
{parseFloat(order.tax_total || 0) > 0 && (
<div className="flex justify-between text-sm">
<span>TAX:</span>
<span className="font-mono">{formatPrice(parseFloat(order.tax_total))}</span>
</div>
)}
<div className="flex justify-between text-lg font-bold border-t-2 border-gray-900 pt-2 mt-2">
<span>TOTAL:</span>
<span className="font-mono">{formatPrice(parseFloat(order.total || 0))}</span>
</div>
</div>
{/* Payment & Status Info */}
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Payment Method:</span>
<span className="font-medium uppercase">{order.payment_method || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Status:</span>
<span className="font-medium uppercase">{getStatusLabel(order.status)}</span>
</div>
</div>
{/* Customer Info */}
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4">
<div className="text-xs text-gray-600 uppercase mb-2">Bill To:</div>
<div className="text-sm">
<div className="font-medium">
{order.billing?.first_name} {order.billing?.last_name}
</div>
<div className="text-gray-600">{order.billing?.email}</div>
{order.billing?.phone && (
<div className="text-gray-600">{order.billing.phone}</div>
)}
</div>
</div>
</div>
)}
{/* Receipt Footer */}
<div className="border-t-2 border-dashed border-gray-400 p-8 text-center bg-gray-50">
<p className="text-sm text-gray-600 mb-4">
{order.status === 'pending'
? 'Awaiting payment confirmation'
: 'Thank you for your business!'}
</p>
{elements.continue_shopping_button && (
<Link to="/shop">
<Button size="lg" className="gap-2">
<ShoppingBag className="w-5 h-5" />
Continue Shopping
</Button>
</Link>
)}
</div>
</div>
{/* Related Products */}
{elements.related_products && relatedProducts.length > 0 && (
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{relatedProducts.map((product: any) => (
<Link key={product.id} to={`/product/${product.slug}`} className="group no-underline">
<div className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-square bg-gray-100 flex items-center justify-center">
{product.image ? (
<img src={product.image} alt={product.name} className="w-full !h-full object-cover" />
) : (
<Package className="w-12 h-12 text-gray-400" />
)}
</div>
<div className="p-3">
<h3 className="font-medium text-sm text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2">
{product.name}
</h3>
<p className="text-sm font-bold text-gray-900 mt-1">
{formatPrice(parseFloat(product.price || 0))}
</p>
</div>
</div>
</Link>
))}
</div>
</div>
)}
</Container>
</div>
);
}
// Render basic style template (default)
return (
<div style={{ backgroundColor }}>
<Container>
<div className="py-12 max-w-3xl mx-auto">
{/* Success Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Order Confirmed!</h1>
<p className="text-gray-600">Order #{order.number}</p>
</div>
{/* Custom Message */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
<p className="text-gray-800 text-center">{customMessage}</p>
</div>
{/* Order Details */}
{elements.order_details && (
<div className="bg-white border rounded-lg p-6 mb-6">
<h2 className="text-xl font-bold mb-4">Order Details</h2>
{/* Order Items */}
<div className="space-y-4 mb-6">
{order.items.map((item: any) => (
<div key={item.id} className="flex items-center gap-4 pb-4 border-b last:border-0">
<div className="w-16 h-16 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0">
{item.image && typeof item.image === 'string' ? (
<img src={item.image} alt={item.name} className="w-full !h-full object-cover rounded-lg" />
) : (
<Package className="w-8 h-8 text-gray-400" />
)}
</div>
<div className="flex-1">
<h3 className="font-medium text-gray-900">{item.name}</h3>
<p className="text-sm text-gray-500">Quantity: {item.qty}</p>
</div>
<div className="text-right">
<p className="font-medium text-gray-900">{formatPrice(item.total)}</p>
</div>
</div>
))}
</div>
{/* Order Summary */}
<div className="border-t pt-4 space-y-2">
<div className="flex justify-between text-gray-600">
<span>Subtotal</span>
<span>{formatPrice(parseFloat(order.subtotal || 0))}</span>
</div>
{parseFloat(order.shipping_total || 0) > 0 && (
<div className="flex justify-between text-gray-600">
<span>Shipping</span>
<span>{formatPrice(parseFloat(order.shipping_total))}</span>
</div>
)}
{parseFloat(order.tax_total || 0) > 0 && (
<div className="flex justify-between text-gray-600">
<span>Tax</span>
<span>{formatPrice(parseFloat(order.tax_total))}</span>
</div>
)}
<div className="flex justify-between font-bold text-lg text-gray-900 pt-2 border-t">
<span>Total</span>
<span>{formatPrice(parseFloat(order.total || 0))}</span>
</div>
</div>
{/* Customer Info */}
<div className="mt-6 pt-6 border-t">
<h3 className="font-medium text-gray-900 mb-3">Customer Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-500 mb-1">Email</p>
<p className="text-gray-900">{order.billing?.email || 'N/A'}</p>
</div>
<div>
<p className="text-gray-500 mb-1">Phone</p>
<p className="text-gray-900">{order.billing?.phone || 'N/A'}</p>
</div>
</div>
</div>
{/* Order Status */}
<div className="mt-6 pt-6 border-t">
<div className="flex items-center gap-3">
<Truck className="w-5 h-5 text-blue-600" />
<div>
<p className="font-medium text-gray-900">Order Status: {getStatusLabel(order.status)}</p>
<p className="text-sm text-gray-500">
{order.status === 'pending' ? 'Awaiting payment confirmation' : "We'll send you shipping updates via email"}
</p>
</div>
</div>
</div>
</div>
)}
{/* Action Buttons */}
<div className="text-center flex flex-col sm:flex-row gap-3 justify-center">
{elements.continue_shopping_button && (
<Link to="/shop">
<Button size="lg" className="gap-2">
<ShoppingBag className="w-5 h-5" />
Continue Shopping
</Button>
</Link>
)}
{isLoggedIn && (
<Link to="/my-account">
<Button size="lg" variant="outline" className="gap-2">
<User className="w-5 h-5" />
Go to Account
</Button>
</Link>
)}
</div>
{/* Related Products */}
{elements.related_products && relatedProducts.length > 0 && (
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{relatedProducts.map((product: any) => (
<Link key={product.id} to={`/product/${product.slug}`} className="group no-underline">
<div className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-square bg-gray-100 flex items-center justify-center">
{product.image ? (
<img src={product.image} alt={product.name} className="w-full !h-full object-cover" />
) : (
<Package className="w-12 h-12 text-gray-400" />
)}
</div>
<div className="p-3">
<h3 className="font-medium text-sm text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2">
{product.name}
</h3>
<p className="text-sm font-bold text-gray-900 mt-1">
{formatPrice(parseFloat(product.price || 0))}
</p>
</div>
</div>
</Link>
))}
</div>
</div>
)}
</div>
</Container>
</div>
);
}