Matt 7faeb9b3f0
Some checks failed
Build and Push Docker Images / docker (push) Failing after 2m8s
Fix TypeScript type error in demo page - change project IDs from number to string
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>
2025-11-03 15:45:34 +01:00

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>
</>
);
}