235 lines
8.7 KiB
TypeScript
235 lines
8.7 KiB
TypeScript
|
|
import { useEffect, useState } from 'react';
|
|||
|
|
import { motion } from 'framer-motion';
|
|||
|
|
import { getOrderDetails } from '../api/checkoutClient';
|
|||
|
|
import { OrderDetailsResponse } from '../types';
|
|||
|
|
|
|||
|
|
export default function CheckoutSuccess() {
|
|||
|
|
const [orderDetails, setOrderDetails] = useState<OrderDetailsResponse | null>(null);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const fetchOrderDetails = async () => {
|
|||
|
|
// Get session ID from URL
|
|||
|
|
const params = new URLSearchParams(window.location.search);
|
|||
|
|
const sessionId = params.get('session_id');
|
|||
|
|
|
|||
|
|
if (!sessionId) {
|
|||
|
|
setError('No session ID found in URL');
|
|||
|
|
setLoading(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const details = await getOrderDetails(sessionId);
|
|||
|
|
setOrderDetails(details);
|
|||
|
|
} catch (err) {
|
|||
|
|
setError('Failed to load order details');
|
|||
|
|
console.error(err);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
fetchOrderDetails();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50 flex items-center justify-center p-6">
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|||
|
|
animate={{ opacity: 1, scale: 1 }}
|
|||
|
|
className="text-center"
|
|||
|
|
>
|
|||
|
|
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto"></div>
|
|||
|
|
<p className="mt-4 text-slate-600 font-medium">Loading your order details...</p>
|
|||
|
|
</motion.div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error || !orderDetails) {
|
|||
|
|
return (
|
|||
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50 flex items-center justify-center p-6">
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, y: 20 }}
|
|||
|
|
animate={{ opacity: 1, y: 0 }}
|
|||
|
|
className="max-w-md w-full bg-white rounded-2xl shadow-luxury p-8 text-center"
|
|||
|
|
>
|
|||
|
|
<div className="text-red-500 text-5xl mb-4">⚠️</div>
|
|||
|
|
<h2 className="text-2xl font-bold text-slate-800 mb-2">Order Not Found</h2>
|
|||
|
|
<p className="text-slate-600 mb-6">{error || 'Unable to retrieve order details'}</p>
|
|||
|
|
<a
|
|||
|
|
href="/"
|
|||
|
|
className="inline-block px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
|||
|
|
>
|
|||
|
|
Return to Home
|
|||
|
|
</a>
|
|||
|
|
</motion.div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { order, session } = orderDetails;
|
|||
|
|
const totalAmount = order.totalAmount / 100; // Convert cents to dollars
|
|||
|
|
const baseAmount = order.baseAmount / 100;
|
|||
|
|
const processingFee = order.processingFee / 100;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50 flex items-center justify-center p-6">
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, y: 20 }}
|
|||
|
|
animate={{ opacity: 1, y: 0 }}
|
|||
|
|
transition={{ duration: 0.5 }}
|
|||
|
|
className="max-w-2xl w-full"
|
|||
|
|
>
|
|||
|
|
{/* Success Header */}
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ scale: 0 }}
|
|||
|
|
animate={{ scale: 1 }}
|
|||
|
|
transition={{ delay: 0.2, type: 'spring', stiffness: 200 }}
|
|||
|
|
className="text-center mb-8"
|
|||
|
|
>
|
|||
|
|
<div className="text-green-500 text-7xl mb-4">✓</div>
|
|||
|
|
<h1 className="text-4xl md:text-5xl font-bold text-slate-800 mb-2">
|
|||
|
|
Payment Successful!
|
|||
|
|
</h1>
|
|||
|
|
<p className="text-xl text-slate-600">
|
|||
|
|
Thank you for offsetting your carbon footprint
|
|||
|
|
</p>
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
{/* Order Details Card */}
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, y: 20 }}
|
|||
|
|
animate={{ opacity: 1, y: 0 }}
|
|||
|
|
transition={{ delay: 0.3 }}
|
|||
|
|
className="bg-white rounded-2xl shadow-luxury p-8 mb-6"
|
|||
|
|
>
|
|||
|
|
<h2 className="text-2xl font-bold text-slate-800 mb-6">Order Summary</h2>
|
|||
|
|
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{/* Offset Amount */}
|
|||
|
|
<div className="flex justify-between items-center py-3 border-b border-slate-100">
|
|||
|
|
<span className="text-slate-600 font-medium">Carbon Offset</span>
|
|||
|
|
<span className="text-slate-800 font-bold text-lg">
|
|||
|
|
{order.tons} tons CO₂
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Portfolio */}
|
|||
|
|
<div className="flex justify-between items-center py-3 border-b border-slate-100">
|
|||
|
|
<span className="text-slate-600 font-medium">Portfolio</span>
|
|||
|
|
<span className="text-slate-800 font-semibold">
|
|||
|
|
Portfolio #{order.portfolioId}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Base Amount */}
|
|||
|
|
<div className="flex justify-between items-center py-3">
|
|||
|
|
<span className="text-slate-600">Offset Cost</span>
|
|||
|
|
<span className="text-slate-800 font-semibold">
|
|||
|
|
${baseAmount.toFixed(2)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Processing Fee */}
|
|||
|
|
<div className="flex justify-between items-center py-3 border-b border-slate-200">
|
|||
|
|
<span className="text-slate-600">Processing Fee (3%)</span>
|
|||
|
|
<span className="text-slate-800 font-semibold">
|
|||
|
|
${processingFee.toFixed(2)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Total */}
|
|||
|
|
<div className="flex justify-between items-center py-4 bg-gradient-to-r from-blue-50 to-cyan-50 rounded-lg px-4">
|
|||
|
|
<span className="text-slate-800 font-bold text-lg">Total Paid</span>
|
|||
|
|
<span className="text-blue-600 font-bold text-2xl">
|
|||
|
|
${totalAmount.toFixed(2)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Order Info */}
|
|||
|
|
<div className="mt-6 pt-6 border-t border-slate-200">
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|||
|
|
<div>
|
|||
|
|
<span className="text-slate-500">Order ID</span>
|
|||
|
|
<p className="text-slate-800 font-mono text-xs mt-1">{order.id}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-slate-500">Status</span>
|
|||
|
|
<p className="mt-1">
|
|||
|
|
<span className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${
|
|||
|
|
order.status === 'fulfilled'
|
|||
|
|
? 'bg-green-100 text-green-700'
|
|||
|
|
: order.status === 'paid'
|
|||
|
|
? 'bg-blue-100 text-blue-700'
|
|||
|
|
: 'bg-yellow-100 text-yellow-700'
|
|||
|
|
}`}>
|
|||
|
|
{order.status.toUpperCase()}
|
|||
|
|
</span>
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
{session.customerEmail && (
|
|||
|
|
<div className="md:col-span-2">
|
|||
|
|
<span className="text-slate-500">Email</span>
|
|||
|
|
<p className="text-slate-800 mt-1">{session.customerEmail}</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
{/* Impact Message */}
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, y: 20 }}
|
|||
|
|
animate={{ opacity: 1, y: 0 }}
|
|||
|
|
transition={{ delay: 0.4 }}
|
|||
|
|
className="bg-gradient-to-r from-green-500 to-emerald-500 rounded-2xl shadow-luxury p-6 text-white text-center mb-6"
|
|||
|
|
>
|
|||
|
|
<h3 className="text-2xl font-bold mb-2">🌍 Making an Impact</h3>
|
|||
|
|
<p className="text-green-50 text-lg">
|
|||
|
|
You've offset {order.tons} tons of CO₂ - equivalent to planting approximately{' '}
|
|||
|
|
{Math.round(order.tons * 50)} trees!
|
|||
|
|
</p>
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
{/* Action Buttons */}
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0 }}
|
|||
|
|
animate={{ opacity: 1 }}
|
|||
|
|
transition={{ delay: 0.5 }}
|
|||
|
|
className="flex flex-col sm:flex-row gap-4 justify-center"
|
|||
|
|
>
|
|||
|
|
<a
|
|||
|
|
href="/"
|
|||
|
|
className="px-8 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all hover:shadow-lg font-semibold text-center"
|
|||
|
|
>
|
|||
|
|
Return to Home
|
|||
|
|
</a>
|
|||
|
|
<button
|
|||
|
|
onClick={() => window.print()}
|
|||
|
|
className="px-8 py-3 bg-white text-slate-700 rounded-lg hover:bg-slate-50 transition-all hover:shadow-lg font-semibold border border-slate-200"
|
|||
|
|
>
|
|||
|
|
Print Receipt
|
|||
|
|
</button>
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
{/* Confirmation Email Notice */}
|
|||
|
|
{session.customerEmail && (
|
|||
|
|
<motion.p
|
|||
|
|
initial={{ opacity: 0 }}
|
|||
|
|
animate={{ opacity: 1 }}
|
|||
|
|
transition={{ delay: 0.6 }}
|
|||
|
|
className="text-center text-slate-500 text-sm mt-6"
|
|||
|
|
>
|
|||
|
|
A confirmation email has been sent to {session.customerEmail}
|
|||
|
|
</motion.p>
|
|||
|
|
)}
|
|||
|
|
</motion.div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|