2025-06-03 17:07:59 +02:00
import React , { useState , useEffect , useCallback } from 'react' ;
2025-10-29 21:45:14 +01:00
import { Check , AlertCircle , ArrowLeft , Loader2 , Globe2 , TreePine , Waves , Factory , Wind , X , User , Mail , Phone , Building , Flame , Snowflake , Mountain , Sprout , Package , Droplet , Leaf , Zap } from 'lucide-react' ;
2025-06-03 18:29:34 +02:00
import { motion , AnimatePresence } from 'framer-motion' ;
2025-10-29 21:45:14 +01:00
import { getPortfolios } from '../api/wrenClient' ;
import { createCheckoutSession } from '../api/checkoutClient' ;
import type { OffsetOrder as OffsetOrderType , Portfolio , OffsetProject } from '../types' ;
import { currencies , formatCurrency } from '../utils/currencies' ;
2025-05-13 18:50:30 +02:00
import { config } from '../utils/config' ;
import { sendFormspreeEmail } from '../utils/email' ;
2025-10-29 14:58:22 +01:00
import { logger } from '../utils/logger' ;
2025-10-29 21:45:14 +01:00
import { FormInput } from './forms/FormInput' ;
import { FormTextarea } from './forms/FormTextarea' ;
import { RadialProgress } from './RadialProgress' ;
import { PortfolioDonutChart } from './PortfolioDonutChart' ;
import { getProjectColor } from '../utils/portfolioColors' ;
import { CertificationBadge } from './CertificationBadge' ;
2025-10-30 13:55:51 +01:00
import { CarbonImpactComparison } from './CarbonImpactComparison' ;
import { useCalculatorState } from '../hooks/useCalculatorState' ;
2025-05-13 18:50:30 +02:00
interface Props {
tons : number ;
monetaryAmount? : number ;
onBack : ( ) = > void ;
calculatorType : 'trip' | 'annual' ;
}
interface ProjectTypeIconProps {
project : OffsetProject ;
}
const ProjectTypeIcon = ( { project } : ProjectTypeIconProps ) = > {
2025-10-29 21:45:14 +01:00
// Safely check if project exists
if ( ! project || ! project . name ) {
2025-05-13 18:50:30 +02:00
return < Globe2 className = "text-blue-500" / > ;
}
2025-10-29 21:45:14 +01:00
const name = project . name . toLowerCase ( ) ;
// Match on project name to determine appropriate icon
if ( name . includes ( 'rainforest' ) || name . includes ( 'forest' ) ) {
return < TreePine className = "text-green-600" / > ;
} else if ( name . includes ( 'biochar' ) && name . includes ( 'carbon removal' ) ) {
return < Flame className = "text-orange-600" / > ;
} else if ( name . includes ( 'refrigerant' ) ) {
return < Snowflake className = "text-cyan-600" / > ;
} else if ( name . includes ( 'rock weathering' ) || name . includes ( 'enhanced weathering' ) ) {
return < Mountain className = "text-amber-700" / > ;
} else if ( name . includes ( 'rice paddies' ) || name . includes ( 'paddies' ) ) {
return < Sprout className = "text-green-500" / > ;
} else if ( name . includes ( 'mangrove' ) || name . includes ( 'blue carbon' ) ) {
return < Waves className = "text-blue-600" / > ;
} else if ( name . includes ( 'biomass storage' ) || name . includes ( 'underground' ) ) {
return < Package className = "text-amber-800" / > ;
} else if ( name . includes ( 'bio-oil' ) || name . includes ( 'oil' ) ) {
return < Droplet className = "text-blue-700" / > ;
} else if ( name . includes ( 'adipic' ) || name . includes ( 'nitrous oxide' ) || name . includes ( 'factory' ) ) {
return < Factory className = "text-gray-600" / > ;
} else if ( name . includes ( 'renewable' ) || name . includes ( 'wind' ) || name . includes ( 'solar' ) ) {
return < Wind className = "text-green-500" / > ;
} else if ( name . includes ( 'direct air capture' ) || name . includes ( 'dac' ) ) {
return < Zap className = "text-purple-600" / > ;
} else {
return < Globe2 className = "text-blue-500" / > ;
2025-05-13 18:50:30 +02:00
}
} ;
export function OffsetOrder ( { tons , monetaryAmount , onBack , calculatorType } : Props ) {
2025-10-30 13:55:51 +01:00
const { state : savedState , saveState } = useCalculatorState ( ) ;
2025-05-13 18:50:30 +02:00
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 [ portfolio , setPortfolio ] = useState < Portfolio | null > ( null ) ;
const [ loadingPortfolio , setLoadingPortfolio ] = useState ( true ) ;
2025-10-30 13:55:51 +01:00
const [ offsetPercentage , setOffsetPercentage ] = useState (
savedState ? . offsetPercentage ? ? 100 // Default to 100% or use saved value
) ;
2025-10-29 12:51:43 +01:00
// 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-10-30 13:55:51 +01:00
// Save offset percentage and portfolio ID to localStorage
useEffect ( ( ) = > {
if ( savedState ) {
saveState ( {
. . . savedState ,
offsetPercentage ,
portfolioId : portfolio?.id ,
} ) ;
}
} , [ offsetPercentage , portfolio , savedState , saveState ] ) ;
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-10-29 14:58:22 +01:00
logger . 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-10-29 14:58:22 +01:00
logger . 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 21:45:14 +01:00
logger . info ( '[OffsetOrder] Creating checkout session for' , actualOffsetTons , 'tons with portfolio' , portfolio . id ) ;
// Create Stripe Checkout Session
const checkoutSession = await createCheckoutSession ( {
tons : actualOffsetTons ,
portfolioId : portfolio.id ,
2025-10-30 14:17:14 +01:00
pricePerTon : roundedPricePerTon , // Pass the rounded-up price that matches calculator display
2025-10-29 21:45:14 +01:00
} ) ;
logger . info ( '[OffsetOrder] Checkout session created:' , checkoutSession . sessionId ) ;
// Redirect to Stripe Checkout
window . location . href = checkoutSession . url ;
2025-05-13 18:50:30 +02:00
} catch ( err ) {
2025-10-29 21:45:14 +01:00
logger . error ( '[OffsetOrder] Failed to create checkout session:' , err ) ;
setError ( 'Failed to create checkout session. Please try again.' ) ;
2025-05-13 18:50:30 +02:00
setLoading ( false ) ;
}
} ;
const renderPortfolioPrice = ( portfolio : Portfolio ) = > {
try {
2025-10-30 13:05:52 +01:00
// Get the price per ton from the portfolio and round UP to next whole number
const pricePerTon = Math . ceil ( portfolio . pricePerTon || 18 ) ;
2025-10-29 21:45:14 +01:00
return formatCurrency ( pricePerTon , currencies . USD ) ;
2025-05-13 18:50:30 +02:00
} catch ( err ) {
console . error ( 'Error formatting portfolio price:' , err ) ;
2025-10-30 14:17:14 +01:00
return formatCurrency ( 18 , currencies . USD ) ;
2025-05-13 18:50:30 +02:00
}
} ;
2025-10-30 14:17:14 +01:00
// Calculate offset cost using the portfolio price (rounded UP to match display)
2025-10-30 13:05:52 +01:00
const roundedPricePerTon = portfolio ? Math . ceil ( portfolio . pricePerTon || 18 ) : 18 ;
2025-10-30 14:17:14 +01:00
const offsetCost = monetaryAmount || ( portfolio ? actualOffsetTons * roundedPricePerTon : 0 ) ;
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" >
2025-10-29 21:45:14 +01:00
< FormInput
id = "offset-name"
label = "Name *"
icon = { < User size = { 20 } / > }
type = "text"
required
value = { formData . name }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , name : e.target.value } ) ) }
/ >
< FormInput
id = "offset-email"
label = "Email *"
icon = { < Mail size = { 20 } / > }
type = "email"
required
value = { formData . email }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , email : e.target.value } ) ) }
/ >
< FormInput
id = "offset-phone"
label = "Phone"
icon = { < Phone size = { 20 } / > }
type = "tel"
value = { formData . phone }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , phone : e.target.value } ) ) }
/ >
< FormInput
id = "offset-company"
label = "Company"
icon = { < Building size = { 20 } / > }
type = "text"
value = { formData . company }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , company : e.target.value } ) ) }
/ >
< FormTextarea
id = "offset-message"
label = "Message"
rows = { 4 }
value = { formData . message }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , message : e.target.value } ) ) }
/ >
2025-05-13 18:50:30 +02:00
< 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 >
2025-10-29 21:45:14 +01:00
< p className = "text-gray-600 mb-0" >
2025-05-13 18:50:30 +02:00
{ portfolio . description }
< / p >
2025-10-29 21:45:14 +01:00
{ /* Portfolio Allocation Visualization */ }
2025-05-13 18:50:30 +02:00
{ portfolio . projects && portfolio . projects . length > 0 && (
2025-10-29 21:45:14 +01:00
< motion.div
className = "bg-white rounded-lg px-6 pt-4 pb-6 shadow-md border border-gray-200 mb-8 mt-6"
initial = { { opacity : 0 , y : 20 } }
animate = { { opacity : 1 , y : 0 } }
transition = { { duration : 0.5 , delay : 0.2 } }
>
< h4 className = "text-xl font-semibold text-gray-900 mb-0 md:mb-0 text-center" > Portfolio Distribution < / h4 >
< div className = "mt-2 md:-mt-12" >
< PortfolioDonutChart
projects = { portfolio . projects }
totalTons = { actualOffsetTons }
/ >
< / div >
< / motion.div >
) }
{ /* Project Cards */ }
{ portfolio . projects && portfolio . projects . length > 0 && (
< motion.div
className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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 21:45:14 +01:00
{ /* Header with title and radial progress */ }
2025-10-29 12:51:43 +01:00
< 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 21:45:14 +01:00
< RadialProgress
percentage = { project . percentage * 100 }
size = { 56 }
color = { getProjectColor ( index , portfolio . projects . length ) }
delay = { index * 0.1 + 0.3 }
/ >
2025-06-03 15:21:29 +02:00
) }
< / 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 */ }
2025-10-29 21:45:14 +01:00
< p className = "text-gray-600 mb-3 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 21:45:14 +01:00
< CertificationBadge status = { project . certificationStatus } size = "sm" / >
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 */ }
2025-10-29 21:45:14 +01:00
{ ! monetaryAmount && (
< 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 >
2025-10-29 12:51:43 +01:00
< / div >
< / div >
2025-10-29 21:45:14 +01:00
< 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 >
2025-10-29 12:51:43 +01:00
< / div >
< / div >
2025-10-29 21:45:14 +01:00
< / motion.div >
) }
2025-10-29 12:51:43 +01:00
< 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 } }
2025-10-30 13:55:51 +01:00
transition = { { duration : 0.5 , delay : 0.62 } }
2025-06-03 14:07:33 +02:00
>
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 21:45:14 +01:00
< span className = "font-medium text-right" > { 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 >
2025-10-29 21:45:14 +01:00
< span className = "font-medium text-right" > Automatically optimized < / span >
2025-06-03 14:07:33 +02:00
< / div >
< div className = "flex justify-between" >
< span className = "text-gray-600" > Cost per Ton : < / span >
2025-10-29 21:45:14 +01:00
< span className = "font-medium text-right" > { renderPortfolioPrice ( portfolio ) } < / span >
2025-06-03 14:07:33 +02:00
< / 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 >
2025-10-29 21:45:14 +01:00
< span className = "text-gray-900 font-semibold text-right" >
{ formatCurrency ( offsetCost , currencies . USD ) }
2025-05-13 18:50:30 +02:00
< / 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 } / >
2025-10-29 21:45:14 +01:00
Redirecting to checkout . . .
2025-05-13 18:50:30 +02:00
< / div >
) : (
2025-10-29 21:45:14 +01:00
'Proceed to Checkout'
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
< / >
) : null }
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
}