Some checks failed
Build and Push Docker Images / docker (push) Failing after 2m8s
Changed all project IDs in mock data from numeric type (1, 2, 3, 4) to string type ('1', '2', '3', '4') to match the OffsetProject interface which expects string IDs.
This resolves the Docker build error:
Type 'number' is not assignable to type 'string' at line 312
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
396 lines
16 KiB
TypeScript
396 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { Leaf } from 'lucide-react';
|
|
import { CarbonImpactComparison } from '../../../../src/components/CarbonImpactComparison';
|
|
import { RechartsPortfolioPieChart } from '../../../../src/components/RechartsPortfolioPieChart';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
// Mock order data for demo purposes
|
|
const mockOrderDetails = {
|
|
order: {
|
|
id: 'demo_order_123',
|
|
tons: 5.2,
|
|
portfolioId: 1,
|
|
baseAmount: 9360, // $93.60 in cents
|
|
processingFee: 281, // $2.81 in cents
|
|
totalAmount: 9641, // $96.41 in cents
|
|
currency: 'USD',
|
|
status: 'paid',
|
|
wrenOrderId: 'wren_abc123xyz',
|
|
stripeSessionId: 'cs_test_demo123',
|
|
createdAt: new Date().toISOString(),
|
|
portfolio: {
|
|
id: 1,
|
|
name: 'Puffin Maritime Carbon Portfolio',
|
|
description: 'Curated mix of high-impact verified carbon offset projects',
|
|
projects: [
|
|
{
|
|
id: '1',
|
|
name: 'Rimba Raya Biodiversity Reserve',
|
|
type: 'Forestry',
|
|
description: 'Protecting 64,000 hectares of tropical peat swamp forest in Borneo',
|
|
shortDescription: 'Tropical forest conservation in Borneo',
|
|
pricePerTon: 15,
|
|
percentage: 0.3,
|
|
imageUrl: '/projects/forest.jpg',
|
|
location: 'Borneo, Indonesia',
|
|
verificationStandard: 'VCS, CCB',
|
|
impactMetrics: {
|
|
co2Reduced: 3500000
|
|
}
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'Verified Blue Carbon',
|
|
type: 'Blue Carbon',
|
|
description: 'Coastal wetland restoration for carbon sequestration',
|
|
shortDescription: 'Coastal wetland restoration',
|
|
pricePerTon: 25,
|
|
percentage: 0.25,
|
|
imageUrl: '/projects/ocean.jpg',
|
|
location: 'Global',
|
|
verificationStandard: 'VCS',
|
|
impactMetrics: {
|
|
co2Reduced: 1200000
|
|
}
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'Direct Air Capture Technology',
|
|
type: 'Direct Air Capture',
|
|
description: 'Advanced technology removing CO2 directly from atmosphere',
|
|
shortDescription: 'Direct air capture technology',
|
|
pricePerTon: 35,
|
|
percentage: 0.2,
|
|
imageUrl: '/projects/tech.jpg',
|
|
location: 'Iceland',
|
|
verificationStandard: 'ISO 14064',
|
|
impactMetrics: {
|
|
co2Reduced: 500000
|
|
}
|
|
},
|
|
{
|
|
id: '4',
|
|
name: 'Renewable Energy Development',
|
|
type: 'Renewable Energy',
|
|
description: 'Wind and solar power generation replacing fossil fuels',
|
|
shortDescription: 'Clean energy generation',
|
|
pricePerTon: 12,
|
|
percentage: 0.25,
|
|
imageUrl: '/projects/solar.jpg',
|
|
location: 'Global',
|
|
verificationStandard: 'Gold Standard',
|
|
impactMetrics: {
|
|
co2Reduced: 2800000
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
session: {
|
|
paymentStatus: 'paid',
|
|
customerEmail: 'demo@puffinoffset.com'
|
|
}
|
|
};
|
|
|
|
// Map backend status to user-friendly labels
|
|
const getStatusDisplay = (status: string): { label: string; className: string } => {
|
|
switch (status) {
|
|
case 'paid':
|
|
case 'fulfilled':
|
|
return { label: 'Confirmed', className: 'bg-green-100 text-green-700' };
|
|
case 'pending':
|
|
return { label: 'Processing', className: 'bg-yellow-100 text-yellow-700' };
|
|
default:
|
|
return { label: status.toUpperCase(), className: 'bg-slate-100 text-slate-700' };
|
|
}
|
|
};
|
|
|
|
// Format currency with commas
|
|
const formatCurrency = (amount: number): string => {
|
|
return amount.toLocaleString('en-US', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
});
|
|
};
|
|
|
|
export default function CheckoutSuccessDemoPage() {
|
|
const router = useRouter();
|
|
const orderDetails = mockOrderDetails;
|
|
const { order, session } = orderDetails;
|
|
const totalAmount = order.totalAmount / 100;
|
|
const baseAmount = order.baseAmount / 100;
|
|
const processingFee = order.processingFee / 100;
|
|
const effectiveStatus = session.paymentStatus === 'paid' ? 'paid' : order.status;
|
|
const statusDisplay = getStatusDisplay(effectiveStatus);
|
|
|
|
return (
|
|
<>
|
|
{/* Print-specific styles */}
|
|
<style>{`
|
|
@media print {
|
|
* {
|
|
-webkit-print-color-adjust: exact !important;
|
|
print-color-adjust: exact !important;
|
|
color-adjust: exact !important;
|
|
}
|
|
.no-print { display: none !important; }
|
|
.print-page-break {
|
|
page-break-after: always !important;
|
|
break-after: page !important;
|
|
}
|
|
@page {
|
|
margin: 0.5in;
|
|
size: letter;
|
|
}
|
|
}
|
|
`}</style>
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50 flex items-center justify-center p-4 sm:p-6">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
className="max-w-full sm:max-w-4xl lg:max-w-5xl w-full print-receipt"
|
|
>
|
|
{/* Demo Banner */}
|
|
<div className="bg-yellow-100 border-l-4 border-yellow-500 p-4 mb-4 rounded-lg no-print">
|
|
<p className="text-yellow-800 font-medium">
|
|
🎭 <strong>DEMO MODE</strong> - This is a mock order for visual comparison purposes
|
|
</p>
|
|
</div>
|
|
|
|
{/* Receipt Container */}
|
|
<div className="bg-white rounded-3xl shadow-2xl overflow-hidden print:rounded-none print:shadow-none print-page-break">
|
|
{/* Header with Logo */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.1 }}
|
|
className="bg-gradient-to-br from-cyan-500 via-blue-500 to-indigo-600 p-8 text-center"
|
|
>
|
|
<img
|
|
src="/puffinOffset.webp"
|
|
alt="Puffin Offset"
|
|
className="h-24 mx-auto mb-4 print-logo"
|
|
/>
|
|
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2">
|
|
Order Confirmed
|
|
</h1>
|
|
<p className="text-cyan-50 text-lg">
|
|
Thank you for your carbon offset purchase
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Success Badge */}
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ delay: 0.2, type: 'spring', stiffness: 200 }}
|
|
className="flex justify-center -mt-8 mb-6 no-print"
|
|
>
|
|
<div className="bg-green-500 text-white rounded-full p-6 shadow-xl border-4 border-white">
|
|
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Order Details Section */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.3 }}
|
|
className="px-8 py-6"
|
|
>
|
|
<h2 className="text-2xl font-bold text-slate-800 mb-6 pb-3 border-b-2 border-slate-200">
|
|
Order Summary
|
|
</h2>
|
|
|
|
<div className="space-y-1 mb-6">
|
|
{/* Carbon Offset - Highlighted */}
|
|
<div className="bg-gradient-to-r from-emerald-50 to-teal-50 rounded-xl p-6 mb-4 border-l-4 border-emerald-500">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<span className="text-sm text-emerald-700 font-medium uppercase tracking-wide">Carbon Offset</span>
|
|
<p className="text-3xl font-bold text-emerald-900 mt-1">{order.tons} tons CO₂</p>
|
|
</div>
|
|
<div className="text-emerald-600">
|
|
<Leaf className="w-16 h-16" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pricing Breakdown */}
|
|
<div className="bg-slate-50 rounded-lg p-5 my-4 space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-slate-700">Offset Cost</span>
|
|
<span className="text-slate-900 font-semibold">
|
|
${formatCurrency(baseAmount)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-slate-700">Processing Fee (3%)</span>
|
|
<span className="text-slate-900 font-semibold">
|
|
${formatCurrency(processingFee)}
|
|
</span>
|
|
</div>
|
|
<div className="border-t-2 border-slate-300 pt-3 mt-3">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-slate-800 font-bold text-lg">Total Paid</span>
|
|
<span className="text-blue-600 font-bold text-3xl">
|
|
${formatCurrency(totalAmount)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order Metadata */}
|
|
<div className="bg-gradient-to-r from-slate-50 to-blue-50 rounded-lg p-5 space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div>
|
|
<span className="text-xs uppercase tracking-wide text-slate-500 font-semibold">Payment ID</span>
|
|
<p className="text-slate-800 font-mono text-xs mt-1 break-all">{order.stripeSessionId}</p>
|
|
</div>
|
|
|
|
{order.wrenOrderId && (
|
|
<div>
|
|
<span className="text-xs uppercase tracking-wide text-slate-500 font-semibold">Offsetting Order ID</span>
|
|
<p className="text-slate-800 font-mono text-xs mt-1 break-all">{order.wrenOrderId}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className={order.wrenOrderId ? '' : 'md:col-start-2'}>
|
|
<span className="text-xs uppercase tracking-wide text-slate-500 font-semibold">Status</span>
|
|
<p className="mt-2">
|
|
<span className={`inline-block px-4 py-1.5 rounded-full text-sm font-bold ${statusDisplay.className}`}>
|
|
{statusDisplay.label}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
{session.customerEmail && (
|
|
<div className="md:col-span-2">
|
|
<span className="text-xs uppercase tracking-wide text-slate-500 font-semibold">Email</span>
|
|
<p className="text-slate-800 font-medium mt-1">{session.customerEmail}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="md:col-span-2">
|
|
<span className="text-xs uppercase tracking-wide text-slate-500 font-semibold">Date</span>
|
|
<p className="text-slate-800 font-medium mt-1">
|
|
{new Date().toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Portfolio Distribution Chart */}
|
|
{order.portfolio?.projects && order.portfolio.projects.length > 0 && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.35 }}
|
|
className="mt-6"
|
|
>
|
|
<div className="bg-white rounded-3xl shadow-2xl overflow-hidden p-8 print:rounded-none print:shadow-none print:border-none print-page-break">
|
|
<h2 className="text-2xl font-bold text-slate-800 mb-2 text-center print:text-xl">
|
|
Your Carbon Offset Distribution
|
|
</h2>
|
|
<p className="text-slate-600 text-center mb-8 print:text-sm print:mb-4">
|
|
Your {order.tons} tons of CO₂ offsets are distributed across these verified projects:
|
|
</p>
|
|
<RechartsPortfolioPieChart
|
|
projects={order.portfolio.projects}
|
|
totalTons={order.tons}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Impact Comparisons */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.4 }}
|
|
className="mt-6"
|
|
>
|
|
<div className="bg-gradient-to-br from-emerald-600 via-teal-600 to-cyan-600 rounded-3xl p-8 shadow-2xl print:rounded-none print:shadow-none print:bg-white print:border print:border-gray-300 print-page-break">
|
|
<CarbonImpactComparison tons={order.tons} variant="success" count={3} />
|
|
</div>
|
|
</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 mt-8 no-print"
|
|
>
|
|
<button
|
|
onClick={() => router.push('/')}
|
|
className="px-8 py-4 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-xl hover:from-blue-600 hover:to-cyan-600 transition-all hover:shadow-xl font-bold text-center transform hover:scale-105"
|
|
>
|
|
Return to Home
|
|
</button>
|
|
<button
|
|
onClick={() => router.push('/calculator')}
|
|
className="px-8 py-4 bg-gradient-to-r from-green-500 to-emerald-500 text-white rounded-xl hover:from-green-600 hover:to-emerald-600 transition-all hover:shadow-xl font-bold text-center transform hover:scale-105"
|
|
>
|
|
Calculate Another Offset
|
|
</button>
|
|
<button
|
|
onClick={() => window.print()}
|
|
className="px-8 py-4 bg-white text-slate-700 rounded-xl hover:bg-slate-50 transition-all hover:shadow-xl font-bold border-2 border-slate-300 flex items-center justify-center gap-2 transform hover:scale-105"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
|
|
</svg>
|
|
Print Receipt
|
|
</button>
|
|
</motion.div>
|
|
|
|
{/* Confirmation Email Notice */}
|
|
{session.customerEmail && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.6 }}
|
|
className="text-center mt-6 no-print"
|
|
>
|
|
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-lg inline-block">
|
|
<p className="text-blue-800 font-medium">
|
|
<svg className="w-5 h-5 inline mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
|
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
|
</svg>
|
|
Confirmation email sent to {session.customerEmail}
|
|
</p>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.7 }}
|
|
className="text-center text-slate-500 text-sm mt-8 pb-6 no-print"
|
|
>
|
|
<p>Thank you for making a positive impact on our planet</p>
|
|
<p className="mt-2">Questions? Contact us at support@puffinoffset.com</p>
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|