2025-06-03 17:07:59 +02:00
import React , { useState , useEffect , useCallback } from 'react' ;
2025-06-03 14:07:33 +02:00
import { Check , AlertCircle , ArrowLeft , Loader2 , Globe2 , TreePine , Waves , Factory , Wind , X } from 'lucide-react' ;
2025-06-03 18:29:34 +02:00
import { motion , AnimatePresence } from 'framer-motion' ;
2025-05-13 18:50:30 +02:00
import { createOffsetOrder , getPortfolios } from '../api/wrenClient' ;
import type { CurrencyCode , OffsetOrder as OffsetOrderType , Portfolio , OffsetProject } from '../types' ;
import { currencies , formatCurrency , getCurrencyByCode } from '../utils/currencies' ;
import { config } from '../utils/config' ;
import { sendFormspreeEmail } from '../utils/email' ;
interface Props {
tons : number ;
monetaryAmount? : number ;
onBack : ( ) = > void ;
calculatorType : 'trip' | 'annual' ;
}
interface ProjectTypeIconProps {
project : OffsetProject ;
}
const ProjectTypeIcon = ( { project } : ProjectTypeIconProps ) = > {
// Safely check if project and type exist
if ( ! project || ! project . type ) {
return < Globe2 className = "text-blue-500" / > ;
}
const type = project . type . toLowerCase ( ) ;
switch ( type ) {
case 'direct air capture' :
return < Factory className = "text-purple-500" / > ;
case 'blue carbon' :
return < Waves className = "text-blue-500" / > ;
case 'renewable energy' :
return < Wind className = "text-green-500" / > ;
case 'forestry' :
return < TreePine className = "text-green-500" / > ;
default :
return < Globe2 className = "text-blue-500" / > ;
}
} ;
export function OffsetOrder ( { tons , monetaryAmount , onBack , calculatorType } : Props ) {
const [ loading , setLoading ] = useState ( false ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
const [ success , setSuccess ] = useState ( false ) ;
const [ order , setOrder ] = useState < OffsetOrderType | null > ( null ) ;
const [ currency , setCurrency ] = useState < CurrencyCode > ( 'USD' ) ;
const [ portfolio , setPortfolio ] = useState < Portfolio | null > ( null ) ;
const [ loadingPortfolio , setLoadingPortfolio ] = useState ( true ) ;
2025-06-03 14:07:33 +02:00
const [ selectedProject , setSelectedProject ] = useState < OffsetProject | null > ( null ) ;
2025-10-29 12:51:43 +01:00
const [ offsetPercentage , setOffsetPercentage ] = useState ( 100 ) ; // Default to 100%
// Calculate the actual tons to offset based on percentage
const actualOffsetTons = ( tons * offsetPercentage ) / 100 ;
// Format tons for display
const formatTons = ( tons : number ) : string = > {
const fixed = tons . toFixed ( 2 ) ;
const parts = fixed . split ( '.' ) ;
parts [ 0 ] = parts [ 0 ] . replace ( /\B(?=(\d{3})+(?!\d))/g , ',' ) ;
return parts . join ( '.' ) ;
} ;
2025-05-13 18:50:30 +02:00
const [ formData , setFormData ] = useState ( {
name : '' ,
email : '' ,
phone : '' ,
company : '' ,
2025-10-29 12:51:43 +01:00
message : ` I would like to offset ${ formatTons ( actualOffsetTons ) } tons of CO2 from my yacht's ${ calculatorType } emissions. `
2025-05-13 18:50:30 +02:00
} ) ;
2025-10-29 12:51:43 +01:00
// Update form message when percentage changes
useEffect ( ( ) = > {
setFormData ( prev = > ( {
. . . prev ,
message : ` I would like to offset ${ formatTons ( actualOffsetTons ) } tons of CO2 ( ${ offsetPercentage } % of ${ formatTons ( tons ) } tons) from my yacht's ${ calculatorType } emissions. `
} ) ) ;
} , [ offsetPercentage , actualOffsetTons , tons , calculatorType ] ) ;
2025-05-13 18:50:30 +02:00
useEffect ( ( ) = > {
if ( ! config . wrenApiKey ) {
setError ( 'Carbon offset service is currently unavailable. Please use our contact form to request offsetting.' ) ;
setLoadingPortfolio ( false ) ;
return ;
}
fetchPortfolio ( ) ;
} , [ ] ) ;
const fetchPortfolio = async ( ) = > {
try {
2025-05-13 20:09:23 +02:00
const allPortfolios = await getPortfolios ( ) ;
// Check if portfolios were returned
if ( ! allPortfolios || allPortfolios . length === 0 ) {
throw new Error ( 'No portfolios available' ) ;
}
2025-05-13 20:21:05 +02:00
// Only get the puffin portfolio, no selection allowed
2025-05-13 20:09:23 +02:00
const puffinPortfolio = allPortfolios . find ( p = >
2025-05-13 18:50:30 +02:00
p . name . toLowerCase ( ) . includes ( 'puffin' ) ||
p . name . toLowerCase ( ) . includes ( 'maritime' )
) ;
2025-05-13 20:09:23 +02:00
if ( puffinPortfolio ) {
2025-05-13 20:21:05 +02:00
console . log ( '[OffsetOrder] Found Puffin portfolio with ID:' , puffinPortfolio . id ) ;
2025-05-13 20:09:23 +02:00
setPortfolio ( puffinPortfolio ) ;
} else {
// Default to first portfolio if no puffin portfolio found
2025-05-13 20:21:05 +02:00
console . log ( '[OffsetOrder] No Puffin portfolio found, using first available portfolio with ID:' , allPortfolios [ 0 ] . id ) ;
2025-05-13 20:09:23 +02:00
setPortfolio ( allPortfolios [ 0 ] ) ;
2025-05-13 18:50:30 +02:00
}
} catch ( err ) {
setError ( 'Failed to fetch portfolio information. Please try again.' ) ;
} finally {
setLoadingPortfolio ( false ) ;
}
} ;
const handleOffsetOrder = async ( ) = > {
if ( ! portfolio ) return ;
2025-10-29 12:51:43 +01:00
2025-05-13 18:50:30 +02:00
setLoading ( true ) ;
setError ( null ) ;
2025-10-29 12:51:43 +01:00
2025-05-13 18:50:30 +02:00
try {
2025-10-29 12:51:43 +01:00
const newOrder = await createOffsetOrder ( portfolio . id , actualOffsetTons ) ;
2025-05-13 18:50:30 +02:00
setOrder ( newOrder ) ;
setSuccess ( true ) ;
} catch ( err ) {
setError ( 'Failed to create offset order. Please try again.' ) ;
} finally {
setLoading ( false ) ;
}
} ;
const renderPortfolioPrice = ( portfolio : Portfolio ) = > {
try {
// Get the price per ton from the portfolio
2025-05-13 20:48:28 +02:00
const pricePerTon = portfolio . pricePerTon || 18 ; // Default based on Wren Climate Fund average
2025-05-13 18:50:30 +02:00
const targetCurrency = getCurrencyByCode ( currency ) ;
return formatCurrency ( pricePerTon , targetCurrency ) ;
} catch ( err ) {
console . error ( 'Error formatting portfolio price:' , err ) ;
2025-05-13 20:48:28 +02:00
return formatCurrency ( 18 , currencies . USD ) ; // Updated fallback
2025-05-13 18:50:30 +02:00
}
} ;
// Calculate offset cost using the portfolio price
2025-10-29 12:51:43 +01:00
const offsetCost = monetaryAmount || ( portfolio ? actualOffsetTons * ( portfolio . pricePerTon || 18 ) : 0 ) ;
2025-05-13 18:50:30 +02:00
2025-06-03 17:07:59 +02:00
// Robust project click handler with multiple fallbacks
const handleProjectClick = useCallback ( ( project : OffsetProject , e? : React.MouseEvent ) = > {
if ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
2025-06-03 15:25:13 +02:00
console . log ( 'Opening project details for:' , project . name ) ;
2025-06-03 15:09:20 +02:00
setSelectedProject ( project ) ;
2025-06-03 17:07:59 +02:00
} , [ ] ) ;
// Additional handler for direct button clicks
const handleProjectButtonClick = useCallback ( ( project : OffsetProject ) = > {
console . log ( 'Button click - Opening project details for:' , project . name ) ;
setSelectedProject ( project ) ;
} , [ ] ) ;
2025-06-03 15:09:20 +02:00
2025-06-03 15:25:13 +02:00
// Simple lightbox close handler
const handleCloseLightbox = ( ) = > {
console . log ( 'Closing lightbox' ) ;
2025-06-03 15:09:20 +02:00
setSelectedProject ( null ) ;
} ;
2025-05-13 18:50:30 +02:00
return (
2025-06-03 14:07:33 +02:00
< motion.div
2025-06-03 18:18:42 +02:00
className = "bg-white rounded-lg shadow-xl p-4 sm:p-8 max-w-7xl w-full relative mx-auto"
2025-06-03 14:07:33 +02:00
initial = { { opacity : 0 , y : 30 } }
animate = { { opacity : 1 , y : 0 } }
transition = { { duration : 0.6 , ease : [ 0.22 , 1 , 0.36 , 1 ] } }
>
< motion.button
2025-05-13 18:50:30 +02:00
onClick = { onBack }
className = "flex items-center text-gray-600 hover:text-gray-900 mb-6"
2025-06-03 14:07:33 +02:00
initial = { { opacity : 0 , x : - 20 } }
animate = { { opacity : 1 , x : 0 } }
transition = { { duration : 0.5 } }
whileHover = { { x : - 5 } }
2025-06-03 15:09:20 +02:00
type = "button"
2025-05-13 18:50:30 +02:00
>
< ArrowLeft className = "mr-2" size = { 20 } / >
Back to Calculator
2025-06-03 14:07:33 +02:00
< / motion.button >
2025-05-13 18:50:30 +02:00
2025-06-03 14:07:33 +02:00
< motion.div
className = "text-center mb-8"
initial = { { opacity : 0 , y : 20 } }
animate = { { opacity : 1 , y : 0 } }
transition = { { duration : 0.5 , delay : 0.2 } }
>
2025-05-13 18:50:30 +02:00
< h2 className = "text-3xl font-bold text-gray-900 mb-4" >
Offset Your Impact
< / h2 >
< p className = "text-lg text-gray-600" >
2025-10-29 12:51:43 +01:00
You ' re about to offset { formatTons ( tons ) } tons of CO ₂
2025-05-13 18:50:30 +02:00
< / p >
2025-06-03 14:07:33 +02:00
< / motion.div >
2025-05-13 18:50:30 +02:00
{ error && ! config . wrenApiKey ? (
< div className = "max-w-2xl mx-auto" >
< div className = "bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8" >
< h3 className = "text-xl font-semibold text-blue-900 mb-4" >
Contact Us for Offsetting
< / h3 >
< p className = "text-blue-700 mb-4" >
Our automated offsetting service is temporarily unavailable . Please fill out the form below and our team will help you offset your emissions .
< / p >
< form onSubmit = { async ( e ) = > {
e . preventDefault ( ) ;
setLoading ( true ) ;
try {
await sendFormspreeEmail ( formData , 'offset' ) ;
setSuccess ( true ) ;
} catch ( err ) {
setError ( 'Failed to send request. Please try again.' ) ;
} finally {
setLoading ( false ) ;
}
} } className = "space-y-6" >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Name *
< / label >
< input
type = "text"
required
value = { formData . name }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , name : e.target.value } ) ) }
className = "w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Email *
< / label >
< input
type = "email"
required
value = { formData . email }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , email : e.target.value } ) ) }
className = "w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Phone
< / label >
< input
type = "tel"
value = { formData . phone }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , phone : e.target.value } ) ) }
className = "w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Company
< / label >
< input
type = "text"
value = { formData . company }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , company : e.target.value } ) ) }
className = "w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Message
< / label >
< textarea
rows = { 4 }
value = { formData . message }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , message : e.target.value } ) ) }
className = "w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/ >
< / div >
< button
type = "submit"
disabled = { loading }
className = { ` w-full flex items-center justify-center bg-blue-500 text-white py-3 rounded-lg transition-colors ${
loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-600'
} ` }
>
{ loading ? (
< >
< Loader2 className = "animate-spin mr-2" size = { 20 } / >
Sending Request . . .
< / >
) : (
'Send Offset Request'
) }
< / button >
< / form >
< / div >
< / div >
) : error ? (
< div className = "bg-red-50 border border-red-200 rounded-lg p-4 mb-6" >
< div className = "flex items-center space-x-2" >
< AlertCircle className = "text-red-500" size = { 20 } / >
< p className = "text-red-700" > { error } < / p >
< / div >
< / div >
) : success && order ? (
< div className = "text-center py-8" >
< div className = "inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-6" >
< Check className = "text-green-500" size = { 32 } / >
< / div >
< h3 className = "text-2xl font-bold text-gray-900 mb-4" >
Offset Order Successful !
< / h3 >
< p className = "text-gray-600 mb-6" >
Your order has been processed successfully . You ' ll receive a confirmation email shortly .
< / p >
< div className = "bg-gray-50 rounded-lg p-6 mb-6" >
< h4 className = "text-lg font-semibold text-gray-900 mb-4" > Order Summary < / h4 >
< div className = "space-y-2" >
< div className = "flex justify-between" >
< span className = "text-gray-600" > Order ID : < / span >
< span className = "font-medium" > { order . id } < / span >
< / div >
< div className = "flex justify-between" >
< span className = "text-gray-600" > Amount : < / span >
< span className = "font-medium" >
{ formatCurrency ( order . amountCharged / 100 , currencies [ order . currency ] ) }
< / span >
< / div >
< div className = "flex justify-between" >
< span className = "text-gray-600" > CO ₂ Offset : < / span >
< span className = "font-medium" > { order . tons } tons < / span >
< / div >
< div className = "flex justify-between" >
< span className = "text-gray-600" > Portfolio : < / span >
< span className = "font-medium" > { order . portfolio . name } < / span >
< / div >
< / div >
< / div >
< button
onClick = { onBack }
className = "bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 transition-colors"
>
Back to Calculator
< / button >
< / div >
) : loadingPortfolio ? (
< div className = "flex justify-center items-center py-12" >
< Loader2 className = "animate-spin text-blue-500" size = { 32 } / >
< span className = "ml-2 text-gray-600" > Loading portfolio information . . . < / span >
< / div >
) : portfolio ? (
< >
< div className = "bg-white border rounded-lg p-6 mb-8" >
< h3 className = "text-xl font-semibold text-gray-900 mb-4" >
{ portfolio . name }
< / h3 >
< p className = "text-gray-600 mb-6" >
{ portfolio . description }
< / p >
{ portfolio . projects && portfolio . projects . length > 0 && (
2025-06-03 14:07:33 +02:00
< motion.div
2025-06-03 18:18:42 +02:00
className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 mb-6"
2025-06-03 14:07:33 +02:00
initial = { { opacity : 0 } }
animate = { { opacity : 1 } }
transition = { { duration : 0.5 , delay : 0.3 } }
>
2025-06-03 17:07:59 +02:00
{ portfolio . projects . map ( ( project , index ) = > (
< motion.div
key = { project . id || ` project- ${ index } ` }
2025-10-29 12:51:43 +01:00
className = "bg-white rounded-lg p-6 shadow-md hover:shadow-xl transition-all border border-gray-200 hover:border-blue-400 relative group flex flex-col h-full"
2025-06-03 17:07:59 +02:00
initial = { { opacity : 0 , y : 20 } }
animate = { { opacity : 1 , y : 0 } }
transition = { { duration : 0.3 , delay : index * 0.1 } }
whileHover = { { scale : 1.02 } }
2025-06-03 15:21:29 +02:00
>
2025-10-29 12:51:43 +01:00
{ /* Header with title and percentage - Fixed height for alignment */ }
< div className = "flex items-start justify-between mb-4 min-h-[60px]" >
< div className = "flex items-start space-x-3 flex-1 pr-2" >
< div className = "mt-1" >
< ProjectTypeIcon project = { project } / >
< / div >
< h4 className = "font-bold text-gray-900 text-lg leading-tight" > { project . name } < / h4 >
2025-06-03 15:21:29 +02:00
< / div >
{ project . percentage && (
2025-10-29 12:51:43 +01:00
< span className = "text-sm bg-blue-100 text-blue-800 font-medium px-3 py-1 rounded-full flex-shrink-0" >
2025-06-03 15:21:29 +02:00
{ ( project . percentage * 100 ) . toFixed ( 1 ) } %
< / span >
) }
< / div >
2025-06-03 17:07:59 +02:00
{ /* Project image */ }
2025-06-03 15:21:29 +02:00
{ project . imageUrl && (
2025-06-03 17:07:59 +02:00
< div className = "relative h-40 mb-4 rounded-lg overflow-hidden" >
2025-06-03 15:21:29 +02:00
< img
src = { project . imageUrl }
alt = { project . name }
2025-06-03 17:07:59 +02:00
className = "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
2025-06-03 15:21:29 +02:00
/ >
2025-06-03 17:07:59 +02:00
< div className = "absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" / >
2025-06-03 15:21:29 +02:00
< / div >
) }
2025-06-03 17:07:59 +02:00
2025-10-29 12:51:43 +01:00
{ /* Description - This will grow to push price and button to bottom */ }
< p className = "text-gray-600 mb-4 leading-relaxed flex-grow" >
2025-06-03 15:21:29 +02:00
{ project . shortDescription || project . description }
< / p >
2025-06-03 17:07:59 +02:00
2025-10-29 12:51:43 +01:00
{ /* Bottom section - Always aligned at the bottom */ }
< div className = "mt-auto" >
{ /* Price info */ }
< div className = "bg-gray-50 p-3 rounded-lg mb-4" >
< div className = "flex justify-between items-center" >
< span className = "text-gray-600 font-medium" > Price per ton : < / span >
< span className = "text-gray-900 font-bold text-lg" >
$ { project . pricePerTon . toFixed ( 2 ) }
< / span >
< / div >
2025-06-03 15:21:29 +02:00
< / div >
2025-06-03 17:07:59 +02:00
2025-10-29 12:51:43 +01:00
{ /* Click button - Primary call to action */ }
< button
onClick = { ( e ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
handleProjectButtonClick ( project ) ;
} }
className = "w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2"
>
< span > View Project Details < / span >
< svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M9 5l7 7-7 7" / >
< / svg >
< / button >
< / div >
2025-06-03 17:07:59 +02:00
< / motion.div >
2025-05-13 18:50:30 +02:00
) ) }
2025-06-03 14:07:33 +02:00
< / motion.div >
2025-05-13 18:50:30 +02:00
) }
2025-06-03 14:07:33 +02:00
< motion.div
className = "flex items-center justify-between bg-blue-50 p-4 rounded-lg"
initial = { { opacity : 0 , scale : 0.95 } }
animate = { { opacity : 1 , scale : 1 } }
transition = { { duration : 0.5 , delay : 0.5 } }
>
2025-05-13 18:50:30 +02:00
< span className = "text-blue-900 font-medium" > Portfolio Price per Ton : < / span >
< span className = "text-blue-900 font-bold text-lg" >
{ renderPortfolioPrice ( portfolio ) }
< / span >
2025-06-03 14:07:33 +02:00
< / motion.div >
2025-05-13 18:50:30 +02:00
< / div >
2025-10-29 12:51:43 +01:00
{ /* Offset Percentage Slider */ }
< motion.div
className = "bg-white border rounded-lg p-6 mb-6"
initial = { { opacity : 0 , y : 20 } }
animate = { { opacity : 1 , y : 0 } }
transition = { { duration : 0.5 , delay : 0.55 } }
>
< h3 className = "text-lg font-semibold text-gray-900 mb-4" > Choose Your Offset Amount < / h3 >
< div className = "space-y-4" >
< div >
< div className = "flex justify-between items-center mb-4" >
< span className = "text-gray-600" > Offset Percentage : < / span >
< span className = "text-2xl font-bold text-blue-600" > { offsetPercentage } % < / span >
< / div >
< div className = "relative" >
{ /* Tick marks - visible notches */ }
< div className = "absolute top-1/2 -translate-y-1/2 left-0 w-full h-2 pointer-events-none flex justify-between items-center" >
{ [ 0 , 25 , 50 , 75 , 100 ] . map ( ( tick ) = > (
< div
key = { tick }
className = "w-[2px] h-2 rounded-full"
style = { {
backgroundColor : tick <= offsetPercentage ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.2)'
} }
> < / div >
) ) }
< / div >
{ /* Slider */ }
< input
type = "range"
min = "0"
max = "100"
value = { offsetPercentage }
onChange = { ( e ) = > setOffsetPercentage ( Number ( e . target . value ) ) }
className = "relative z-10 w-full h-3 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
style = { {
background : ` linear-gradient(to right, #3b82f6 0%, #3b82f6 ${ offsetPercentage } %, #e5e7eb ${ offsetPercentage } %, #e5e7eb 100%) `
} }
/ >
< / div >
{ /* Percentage labels aligned with tick marks */ }
< div className = "relative mt-2 h-4" >
< span className = "text-xs text-gray-500 absolute left-0" > 0 % < / span >
< span className = "text-xs text-gray-500 absolute left-1/4 -translate-x-1/2" > 25 % < / span >
< span className = "text-xs text-gray-500 absolute left-1/2 -translate-x-1/2" > 50 % < / span >
< span className = "text-xs text-gray-500 absolute left-3/4 -translate-x-1/2" > 75 % < / span >
< span className = "text-xs text-gray-500 absolute right-0" > 100 % < / span >
< / div >
< / div >
< div className = "bg-blue-50 p-4 rounded-lg" >
< div className = "flex justify-between items-center" >
< span className = "text-gray-700" > CO ₂ to Offset : < / span >
< span className = "text-xl font-bold text-blue-900" >
{ formatTons ( actualOffsetTons ) } tons
< / span >
< / div >
< div className = "text-sm text-blue-600 mt-1" >
{ offsetPercentage } % of { formatTons ( tons ) } tons total emissions
< / div >
< / div >
< / div >
< / motion.div >
< motion.div
2025-06-03 14:07:33 +02:00
className = "bg-gray-50 rounded-lg p-6 mb-6"
initial = { { opacity : 0 , y : 20 } }
animate = { { opacity : 1 , y : 0 } }
transition = { { duration : 0.5 , delay : 0.6 } }
>
2025-05-13 18:50:30 +02:00
< h3 className = "text-lg font-semibold text-gray-900 mb-4" > Order Summary < / h3 >
2025-06-03 14:07:33 +02:00
< div className = "space-y-4" >
< div className = "flex justify-between" >
< span className = "text-gray-600" > Amount to Offset : < / span >
2025-10-29 12:51:43 +01:00
< span className = "font-medium" > { formatTons ( actualOffsetTons ) } tons CO ₂ < / span >
2025-06-03 14:07:33 +02:00
< / div >
< div className = "flex justify-between" >
< span className = "text-gray-600" > Portfolio Distribution : < / span >
< span className = "font-medium" > Automatically optimized < / span >
< / div >
< div className = "flex justify-between" >
< span className = "text-gray-600" > Cost per Ton : < / span >
< span className = "font-medium" > { renderPortfolioPrice ( portfolio ) } < / span >
< / div >
< div className = "border-t pt-4" >
2025-05-13 18:50:30 +02:00
< div className = "flex justify-between" >
< span className = "text-gray-900 font-semibold" > Total Cost : < / span >
< span className = "text-gray-900 font-semibold" >
{ formatCurrency ( offsetCost , getCurrencyByCode ( portfolio . currency ) ) }
< / span >
< / div >
< / div >
< / div >
2025-06-03 14:07:33 +02:00
< / motion.div >
2025-05-13 18:50:30 +02:00
2025-06-03 14:07:33 +02:00
< motion.button
2025-05-13 18:50:30 +02:00
onClick = { handleOffsetOrder }
disabled = { loading }
className = { ` w-full bg-blue-500 text-white py-3 px-4 rounded-lg transition-colors ${
loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-600'
} ` }
2025-06-03 14:07:33 +02:00
initial = { { opacity : 0 , y : 20 } }
animate = { { opacity : 1 , y : 0 } }
transition = { { duration : 0.5 , delay : 0.7 } }
whileHover = { { scale : 1.02 } }
whileTap = { { scale : 0.98 } }
2025-05-13 18:50:30 +02:00
>
{ loading ? (
< div className = "flex items-center justify-center" >
< Loader2 className = "animate-spin mr-2" size = { 20 } / >
Processing . . .
< / div >
) : (
'Confirm Offset Order'
) }
2025-06-03 14:07:33 +02:00
< / motion.button >
2025-05-13 18:50:30 +02:00
< / >
) : null }
2025-06-03 14:07:33 +02:00
2025-06-03 18:29:34 +02:00
{ /* Animated Lightbox Modal */ }
< AnimatePresence >
{ selectedProject && (
< motion.div
className = "fixed inset-0 z-[9999] flex items-center justify-center p-4"
style = { { backgroundColor : 'rgba(0, 0, 0, 0.8)' } }
onClick = { handleCloseLightbox }
initial = { { opacity : 0 } }
animate = { { opacity : 1 } }
exit = { { opacity : 0 } }
transition = { { duration : 0.3 , ease : [ 0.22 , 1 , 0.36 , 1 ] } }
2025-06-03 15:21:29 +02:00
>
2025-06-03 18:29:34 +02:00
< motion.div
className = "relative bg-white rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick = { ( e ) = > e . stopPropagation ( ) }
initial = { { opacity : 0 , scale : 0.8 , y : 20 } }
animate = { { opacity : 1 , scale : 1 , y : 0 } }
exit = { { opacity : 0 , scale : 0.8 , y : 20 } }
transition = { {
duration : 0.4 ,
ease : [ 0.22 , 1 , 0.36 , 1 ] ,
scale : { type : "spring" , stiffness : 300 , damping : 30 }
} }
>
2025-06-03 15:25:13 +02:00
{ /* Close Button */ }
< button
onClick = { handleCloseLightbox }
className = "absolute top-4 right-4 p-2 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors z-10"
aria - label = "Close details"
2025-06-03 15:21:29 +02:00
>
2025-06-03 15:25:13 +02:00
< X size = { 20 } / >
< / button >
{ /* Project Image */ }
{ selectedProject . imageUrl && (
< div className = "relative h-64 md:h-80 overflow-hidden rounded-t-lg" >
< img
src = { selectedProject . imageUrl }
alt = { selectedProject . name }
className = "w-full h-full object-cover"
/ >
< div className = "absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" / >
< div className = "absolute bottom-4 left-4 right-4" >
< h3 className = "text-2xl font-bold text-white mb-2" > { selectedProject . name } < / h3 >
< div className = "flex items-center space-x-2" >
< ProjectTypeIcon project = { selectedProject } / >
< span className = "text-white/90" > { selectedProject . type || 'Environmental Project' } < / span >
2025-06-03 15:21:29 +02:00
< / div >
< / div >
2025-06-03 15:25:13 +02:00
< / div >
) }
2025-06-03 15:21:29 +02:00
{ /* Project Details */ }
< div className = "p-6" >
{ ! selectedProject . imageUrl && (
< >
< h3 className = "text-2xl font-bold text-gray-900 mb-2" > { selectedProject . name } < / h3 >
< div className = "flex items-center space-x-2 mb-4" >
< ProjectTypeIcon project = { selectedProject } / >
< span className = "text-gray-600" > { selectedProject . type || 'Environmental Project' } < / span >
< / div >
< / >
) }
< p className = "text-gray-700 mb-6" >
{ selectedProject . description || selectedProject . shortDescription }
< / p >
< div className = "grid grid-cols-2 gap-4 mb-6" >
< div className = "bg-gray-50 p-4 rounded-lg" >
< p className = "text-sm text-gray-600 mb-1" > Price per Ton < / p >
< p className = "text-xl font-bold text-gray-900" > $ { selectedProject . pricePerTon . toFixed ( 2 ) } < / p >
< / div >
{ selectedProject . percentage && (
< div className = "bg-gray-50 p-4 rounded-lg" >
< p className = "text-sm text-gray-600 mb-1" > Portfolio Allocation < / p >
< p className = "text-xl font-bold text-gray-900" > { ( selectedProject . percentage * 100 ) . toFixed ( 1 ) } % < / p >
< / div >
) }
< / div >
{ ( selectedProject . location || selectedProject . verificationStandard ) && (
< div className = "space-y-3 mb-6" >
{ selectedProject . location && (
< div className = "flex items-center justify-between" >
< span className = "text-gray-600" > Location : < / span >
< span className = "font-medium text-gray-900" > { selectedProject . location } < / span >
< / div >
) }
{ selectedProject . verificationStandard && (
< div className = "flex items-center justify-between" >
< span className = "text-gray-600" > Verification Standard : < / span >
< span className = "font-medium text-gray-900" > { selectedProject . verificationStandard } < / span >
< / div >
) }
< / div >
) }
{ selectedProject . impactMetrics && selectedProject . impactMetrics . co2Reduced > 0 && (
< div className = "bg-blue-50 p-4 rounded-lg" >
< p className = "text-sm text-blue-700 mb-1" > Impact Metrics < / p >
< p className = "text-lg font-semibold text-blue-900" >
{ selectedProject . impactMetrics . co2Reduced . toLocaleString ( ) } tons CO ₂ reduced
< / p >
< / div >
) }
< / div >
2025-06-03 18:29:34 +02:00
< / motion.div >
< / motion.div >
) }
< / AnimatePresence >
2025-06-03 14:07:33 +02:00
< / motion.div >
2025-05-13 18:50:30 +02:00
) ;
2025-05-13 20:09:23 +02:00
}