Added lightboxes

This commit is contained in:
Matt 2025-06-03 14:07:33 +02:00
parent 5d0cfdef47
commit 2376205371
6 changed files with 858 additions and 307 deletions

49
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"axios": "^1.6.7", "axios": "^1.6.7",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"framer-motion": "^12.15.0",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
@ -3431,6 +3432,33 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "12.15.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.15.0.tgz",
"integrity": "sha512-XKg/LnKExdLGugZrDILV7jZjI599785lDIJZLxMiiIFidCsy0a4R2ZEf+Izm67zyOuJgQYTHOmodi7igQsw3vg==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.15.0",
"motion-utils": "^12.12.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -4500,6 +4528,21 @@
"integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
"dev": true "dev": true
}, },
"node_modules/motion-dom": {
"version": "12.15.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.15.0.tgz",
"integrity": "sha512-D2ldJgor+2vdcrDtKJw48k3OddXiZN1dDLLWrS8kiHzQdYVruh0IoTwbJBslrnTXIPgFED7PBN2Zbwl7rNqnhA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.12.1"
}
},
"node_modules/motion-utils": {
"version": "12.12.1",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz",
"integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5928,6 +5971,12 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true "dev": true
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"axios": "^1.6.7", "axios": "^1.6.7",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"framer-motion": "^12.15.0",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Bird, Menu, X } from 'lucide-react'; import { Bird, Menu, X } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Home } from './components/Home'; import { Home } from './components/Home';
import { YachtSearch } from './components/YachtSearch'; import { YachtSearch } from './components/YachtSearch';
import { TripCalculator } from './components/TripCalculator'; import { TripCalculator } from './components/TripCalculator';
@ -8,7 +9,7 @@ import { About } from './components/About';
import { Contact } from './components/Contact'; import { Contact } from './components/Contact';
import { OffsetOrder } from './components/OffsetOrder'; import { OffsetOrder } from './components/OffsetOrder';
import { getVesselData } from './api/aisClient'; import { getVesselData } from './api/aisClient';
import { calculateCarbon } from './utils/carbonCalculator'; import { calculateTripCarbon } from './utils/carbonCalculator';
import { analytics } from './utils/analytics'; import { analytics } from './utils/analytics';
import type { VesselData, CarbonCalculation, CalculatorType } from './types'; import type { VesselData, CarbonCalculation, CalculatorType } from './types';
@ -211,8 +212,21 @@ function App() {
</div> </div>
</header> </header>
<main className="max-w-7xl mx-auto py-8 sm:py-12"> <main className="max-w-7xl mx-auto py-8 sm:py-12 overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key={currentPage + (showOffsetOrder ? '-offset' : '')}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1.0]
}}
>
{renderPage()} {renderPage()}
</motion.div>
</AnimatePresence>
</main> </main>
<footer className="bg-white mt-16"> <footer className="bg-white mt-16">

View File

@ -1,5 +1,6 @@
import React from 'react'; import React, { useEffect, useRef } from 'react';
import { Anchor, Globe, BarChart } from 'lucide-react'; import { Anchor, Globe, BarChart } from 'lucide-react';
import { motion } from 'framer-motion';
interface Props { interface Props {
onNavigate: (page: 'home' | 'calculator' | 'how-it-works' | 'about' | 'contact') => void; onNavigate: (page: 'home' | 'calculator' | 'how-it-works' | 'about' | 'contact') => void;
@ -20,87 +21,252 @@ export function Home({ onNavigate }: Props) {
}, 0); }, 0);
}; };
// Animation variants
const fadeInUp = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.22, 1, 0.36, 1]
}
}
};
const staggerContainer = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const scaleOnHover = {
rest: { scale: 1 },
hover: {
scale: 1.05,
transition: {
type: "spring",
stiffness: 400,
damping: 17
}
}
};
return ( return (
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="relative mb-16"> <motion.div
className="relative mb-16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8 }}
>
<div className="relative h-[500px]"> <div className="relative h-[500px]">
<img <motion.img
initial={{ scale: 1.1, opacity: 0.8 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 2.5, ease: "easeOut" }}
src="https://images.unsplash.com/photo-1567899378494-47b22a2ae96a?auto=format&fit=crop&q=80" src="https://images.unsplash.com/photo-1567899378494-47b22a2ae96a?auto=format&fit=crop&q=80"
alt="Luxury yacht on calm waters" alt="Luxury yacht on calm waters"
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-transparent to-green-500/20" /> <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.5, delay: 0.5 }}
className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-transparent to-green-500/20"
/>
</div> </div>
<div className="absolute inset-0 flex items-end pb-16"> <div className="absolute inset-0 flex items-end pb-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-5xl font-bold text-white mb-8 drop-shadow-lg"> <motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.8,
delay: 0.2,
ease: [0.22, 1, 0.36, 1]
}}
className="text-5xl font-bold text-white mb-8 drop-shadow-lg"
>
Set Sail Sustainably with Carbon Offsetting for Superyachts Set Sail Sustainably with Carbon Offsetting for Superyachts
</h1> </motion.h1>
<p className="text-xl text-white max-w-3xl mx-auto leading-relaxed drop-shadow-lg"> <motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
delay: 0.6,
ease: [0.22, 1, 0.36, 1]
}}
className="text-xl text-white max-w-3xl mx-auto leading-relaxed drop-shadow-lg"
>
Luxury and environmental responsibility can go hand in hand when you choose to offset the carbon footprint of your superyacht adventures. Luxury and environmental responsibility can go hand in hand when you choose to offset the carbon footprint of your superyacht adventures.
</p> </motion.p>
</div>
</div> </div>
</div> </div>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16"> <motion.div
<div className="bg-white rounded-xl shadow-lg p-8 transform hover:scale-105 transition-transform duration-300"> className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16"
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
>
<motion.div
className="bg-white rounded-xl shadow-lg p-8"
variants={fadeInUp}
initial="rest"
whileInView="rest"
viewport={{ once: true }}
whileHover="hover"
>
<motion.div variants={scaleOnHover} className="h-full">
<div className="flex items-center space-x-4 mb-6"> <div className="flex items-center space-x-4 mb-6">
<motion.div
initial={{ rotate: -10, opacity: 0 }}
whileInView={{ rotate: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
<Globe className="text-blue-600" size={32} /> <Globe className="text-blue-600" size={32} />
</motion.div>
<h2 className="text-2xl font-bold text-gray-900">Flexible Offsetting Solutions</h2> <h2 className="text-2xl font-bold text-gray-900">Flexible Offsetting Solutions</h2>
</div> </div>
<p className="text-gray-600 leading-relaxed text-justify"> <p className="text-gray-600 leading-relaxed text-justify">
With Puffin's carbon offsetting program, it's simple to mitigate the environmental impact of a yacht's use by supporting impactful international projects. Whether you want to offset a portion of a single trip, a season, or a yacht's full annual emissions, Puffin gives you the flexibility to offset as much or as little as you like. With Puffin's carbon offsetting program, it's simple to mitigate the environmental impact of a yacht's use by supporting impactful international projects. Whether you want to offset a portion of a single trip, a season, or a yacht's full annual emissions, Puffin gives you the flexibility to offset as much or as little as you like.
</p> </p>
</div> </motion.div>
</motion.div>
<div className="bg-white rounded-xl shadow-lg p-8 transform hover:scale-105 transition-transform duration-300"> <motion.div
className="bg-white rounded-xl shadow-lg p-8"
variants={fadeInUp}
initial="rest"
whileInView="rest"
viewport={{ once: true }}
whileHover="hover"
>
<motion.div variants={scaleOnHover} className="h-full">
<div className="flex items-center space-x-4 mb-6"> <div className="flex items-center space-x-4 mb-6">
<motion.div
initial={{ rotate: 10, opacity: 0 }}
whileInView={{ rotate: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
viewport={{ once: true }}
>
<BarChart className="text-green-600" size={32} /> <BarChart className="text-green-600" size={32} />
</motion.div>
<h2 className="text-2xl font-bold text-gray-900">Your Values, Your Choice</h2> <h2 className="text-2xl font-bold text-gray-900">Your Values, Your Choice</h2>
</div> </div>
<p className="text-gray-600 leading-relaxed text-justify"> <p className="text-gray-600 leading-relaxed text-justify">
Our portfolios are designed to resonate with the values of our most environmentally-conscious clients, ensuring contributions align with their passion for a better planet. Our science-based, verified carbon offsetting projects have a real and ongoing impact in the fight against climate change. Our portfolios are designed to resonate with the values of our most environmentally-conscious clients, ensuring contributions align with their passion for a better planet. Our science-based, verified carbon offsetting projects have a real and ongoing impact in the fight against climate change.
</p> </p>
</div> </motion.div>
</div> </motion.div>
</motion.div>
<div className="bg-white rounded-xl shadow-lg p-12 mb-16 text-center"> <motion.div
className="bg-white rounded-xl shadow-lg p-12 mb-16 text-center"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, ease: "easeOut" }}
viewport={{ once: true, amount: 0.3 }}
>
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="flex items-center justify-center space-x-4 mb-6"> <div className="flex items-center justify-center space-x-4 mb-6">
<motion.div
initial={{ y: -20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
transition={{
type: "spring",
stiffness: 300,
damping: 15,
delay: 0.2
}}
viewport={{ once: true }}
>
<Anchor className="text-blue-600" size={32} /> <Anchor className="text-blue-600" size={32} />
<h2 className="text-3xl font-bold text-gray-900"> </motion.div>
<motion.h2
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
viewport={{ once: true }}
className="text-3xl font-bold text-gray-900"
>
Empower Your Yacht Business with In-House Offsetting Empower Your Yacht Business with In-House Offsetting
</h2> </motion.h2>
</div> </div>
<p className="text-lg text-gray-600 leading-relaxed text-justify"> <motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
viewport={{ once: true }}
className="text-lg text-gray-600 leading-relaxed text-justify"
>
Our offsetting tool is not only perfect for charter guests and yacht owners, it can also be used by yacht management companies and brokerage firms seeking to integrate sustainability into the entirety of their operations. Use Puffin to offer clients carbon-neutral charter options or manage the environmental footprint of your fleet. Showcase your commitment to eco-conscious luxury while adding value to your services and elevating your brand. Our offsetting tool is not only perfect for charter guests and yacht owners, it can also be used by yacht management companies and brokerage firms seeking to integrate sustainability into the entirety of their operations. Use Puffin to offer clients carbon-neutral charter options or manage the environmental footprint of your fleet. Showcase your commitment to eco-conscious luxury while adding value to your services and elevating your brand.
</p> </motion.p>
</div>
</div> </div>
</motion.div>
<div className="text-center bg-white rounded-xl shadow-lg p-12 mb-16"> <motion.div
<h2 className="text-3xl font-bold text-gray-900 mb-6"> className="text-center bg-white rounded-xl shadow-lg p-12 mb-16"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, ease: "easeOut" }}
viewport={{ once: true, amount: 0.3 }}
>
<motion.h2
initial={{ opacity: 0, y: -10 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
className="text-3xl font-bold text-gray-900 mb-6"
>
Ready to Make a Difference? Ready to Make a Difference?
</h2> </motion.h2>
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto"> <motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
viewport={{ once: true }}
className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto"
>
Join the growing community of environmentally conscious yacht owners and operators who are leading the way in maritime sustainability. Join the growing community of environmentally conscious yacht owners and operators who are leading the way in maritime sustainability.
</p> </motion.p>
<div className="flex justify-center space-x-4"> <motion.div
<button className="flex justify-center space-x-4"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
viewport={{ once: true }}
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
onClick={handleCalculateClick} onClick={handleCalculateClick}
className="bg-blue-600 text-white px-8 py-3 rounded-lg hover:bg-blue-700 transition-colors" className="bg-blue-600 text-white px-8 py-3 rounded-lg"
> >
Calculate Your Impact Calculate Your Impact
</button> </motion.button>
<button <motion.button
whileHover={{ scale: 1.05, backgroundColor: "rgba(219, 234, 254, 1)" }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
onClick={handleLearnMoreClick} onClick={handleLearnMoreClick}
className="border-2 border-blue-600 text-blue-600 px-8 py-3 rounded-lg hover:bg-blue-50 transition-colors" className="border-2 border-blue-600 text-blue-600 px-8 py-3 rounded-lg"
> >
Learn More Learn More
</button> </motion.button>
</div> </motion.div>
</div> </motion.div>
</div> </div>
); );
} }

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Check, AlertCircle, ArrowLeft, Loader2, Globe2, TreePine, Waves, Factory, Wind } from 'lucide-react'; import { Check, AlertCircle, ArrowLeft, Loader2, Globe2, TreePine, Waves, Factory, Wind, X } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { createOffsetOrder, getPortfolios } from '../api/wrenClient'; import { createOffsetOrder, getPortfolios } from '../api/wrenClient';
import type { CurrencyCode, OffsetOrder as OffsetOrderType, Portfolio, OffsetProject } from '../types'; import type { CurrencyCode, OffsetOrder as OffsetOrderType, Portfolio, OffsetProject } from '../types';
import { currencies, formatCurrency, getCurrencyByCode } from '../utils/currencies'; import { currencies, formatCurrency, getCurrencyByCode } from '../utils/currencies';
@ -47,6 +48,7 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
const [currency, setCurrency] = useState<CurrencyCode>('USD'); const [currency, setCurrency] = useState<CurrencyCode>('USD');
const [portfolio, setPortfolio] = useState<Portfolio | null>(null); const [portfolio, setPortfolio] = useState<Portfolio | null>(null);
const [loadingPortfolio, setLoadingPortfolio] = useState(true); const [loadingPortfolio, setLoadingPortfolio] = useState(true);
const [selectedProject, setSelectedProject] = useState<OffsetProject | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
email: '', email: '',
@ -127,23 +129,37 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
const offsetCost = monetaryAmount || (portfolio ? tons * (portfolio.pricePerTon || 18) : 0); const offsetCost = monetaryAmount || (portfolio ? tons * (portfolio.pricePerTon || 18) : 0);
return ( return (
<div className="bg-white rounded-lg shadow-xl p-8 max-w-4xl w-full"> <motion.div
<button className="bg-white rounded-lg shadow-xl p-8 max-w-4xl w-full"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
<motion.button
onClick={onBack} onClick={onBack}
className="flex items-center text-gray-600 hover:text-gray-900 mb-6" className="flex items-center text-gray-600 hover:text-gray-900 mb-6"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
whileHover={{ x: -5 }}
> >
<ArrowLeft className="mr-2" size={20} /> <ArrowLeft className="mr-2" size={20} />
Back to Calculator Back to Calculator
</button> </motion.button>
<div className="text-center mb-8"> <motion.div
className="text-center mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2 className="text-3xl font-bold text-gray-900 mb-4">
Offset Your Impact Offset Your Impact
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600">
You're about to offset {tons.toFixed(2)} tons of CO You're about to offset {tons.toFixed(2)} tons of CO
</p> </p>
</div> </motion.div>
{error && !config.wrenApiKey ? ( {error && !config.wrenApiKey ? (
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto">
@ -306,9 +322,23 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
</p> </p>
{portfolio.projects && portfolio.projects.length > 0 && ( {portfolio.projects && portfolio.projects.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"> <motion.div
{portfolio.projects.map((project) => ( className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"
<div key={project.id} className="bg-gray-50 rounded-lg p-4 hover:shadow-md transition-shadow"> initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
{portfolio.projects.map((project, index) => (
<motion.div
key={project.id}
className="bg-gray-50 rounded-lg p-4 hover:shadow-lg transition-all cursor-pointer"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 * index }}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
onClick={() => setSelectedProject(project)}
>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<ProjectTypeIcon project={project} /> <ProjectTypeIcon project={project} />
@ -325,7 +355,7 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
<img <img
src={project.imageUrl} src={project.imageUrl}
alt={project.name} alt={project.name}
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover transition-transform duration-300 hover:scale-110"
/> />
</div> </div>
)} )}
@ -340,20 +370,33 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
</span> </span>
</div> </div>
</div> </div>
<div className="mt-3 text-center">
<span className="text-xs text-blue-600 font-medium">Click for details</span>
</div> </div>
</motion.div>
))} ))}
</div> </motion.div>
)} )}
<div className="flex items-center justify-between bg-blue-50 p-4 rounded-lg"> <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 }}
>
<span className="text-blue-900 font-medium">Portfolio Price per Ton:</span> <span className="text-blue-900 font-medium">Portfolio Price per Ton:</span>
<span className="text-blue-900 font-bold text-lg"> <span className="text-blue-900 font-bold text-lg">
{renderPortfolioPrice(portfolio)} {renderPortfolioPrice(portfolio)}
</span> </span>
</div> </motion.div>
</div> </div>
<div className="bg-gray-50 rounded-lg p-6 mb-6"> <motion.div
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 }}
>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Order Summary</h3> <h3 className="text-lg font-semibold text-gray-900 mb-4">Order Summary</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between"> <div className="flex justify-between">
@ -377,14 +420,19 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
</div> </div>
</div> </div>
</div> </div>
</div> </motion.div>
<button <motion.button
onClick={handleOffsetOrder} onClick={handleOffsetOrder}
disabled={loading} disabled={loading}
className={`w-full bg-blue-500 text-white py-3 px-4 rounded-lg transition-colors ${ 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' loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-600'
}`} }`}
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 }}
> >
{loading ? ( {loading ? (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
@ -394,9 +442,123 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
) : ( ) : (
'Confirm Offset Order' 'Confirm Offset Order'
)} )}
</button> </motion.button>
</> </>
) : null} ) : null}
{/* Project Lightbox Modal */}
<AnimatePresence>
{selectedProject && (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedProject(null)}
>
{/* Backdrop */}
<motion.div
className="absolute inset-0 bg-black bg-opacity-50"
initial={{ opacity: 0 }}
animate={{ opacity: 0.5 }}
exit={{ opacity: 0 }}
/>
{/* Modal Content */}
<motion.div
className="relative bg-white rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
onClick={(e) => e.stopPropagation()}
>
{/* Close Button */}
<button
onClick={() => setSelectedProject(null)}
className="absolute top-4 right-4 p-2 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<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>
</div> </div>
</div>
</div>
)}
{/* 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>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
); );
} }

View File

@ -1,5 +1,6 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { Route } from 'lucide-react'; import { Route } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import type { VesselData, TripEstimate, CurrencyCode } from '../types'; import type { VesselData, TripEstimate, CurrencyCode } from '../types';
import { calculateTripCarbon, calculateCarbonFromFuel } from '../utils/carbonCalculator'; import { calculateTripCarbon, calculateCarbonFromFuel } from '../utils/carbonCalculator';
import { currencies, formatCurrency } from '../utils/currencies'; import { currencies, formatCurrency } from '../utils/currencies';
@ -70,19 +71,59 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
} }
}, []); }, []);
// Animation variants
const fadeIn = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: 0.5 }
}
};
const slideIn = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: [0.25, 0.1, 0.25, 1.0]
}
}
};
return ( return (
<div className="bg-white rounded-lg shadow-xl p-6 max-w-2xl w-full mt-8"> <motion.div
className="bg-white rounded-lg shadow-xl p-6 max-w-2xl w-full mt-8"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
ease: [0.22, 1, 0.36, 1]
}}
>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-800">Carbon Offset Calculator</h2> <h2 className="text-2xl font-bold text-gray-800">Carbon Offset Calculator</h2>
<motion.div
initial={{ rotate: -10, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Route className="text-blue-500" size={24} /> <Route className="text-blue-500" size={24} />
</motion.div>
</div> </div>
<div className="mb-6"> <motion.div
className="mb-6"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Calculation Method Calculation Method
</label> </label>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<button <motion.button
type="button" type="button"
onClick={() => setCalculationType('fuel')} onClick={() => setCalculationType('fuel')}
className={`px-4 py-2 rounded-lg transition-colors ${ className={`px-4 py-2 rounded-lg transition-colors ${
@ -90,10 +131,13 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`} }`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
> >
Fuel Based Fuel Based
</button> </motion.button>
<button <motion.button
type="button" type="button"
onClick={() => setCalculationType('distance')} onClick={() => setCalculationType('distance')}
className={`px-4 py-2 rounded-lg transition-colors ${ className={`px-4 py-2 rounded-lg transition-colors ${
@ -101,10 +145,13 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`} }`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
> >
Distance Based Distance Based
</button> </motion.button>
<button <motion.button
type="button" type="button"
onClick={() => setCalculationType('custom')} onClick={() => setCalculationType('custom')}
className={`px-4 py-2 rounded-lg transition-colors ${ className={`px-4 py-2 rounded-lg transition-colors ${
@ -112,14 +159,25 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`} }`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
> >
Custom Amount Custom Amount
</button> </motion.button>
</div>
</div> </div>
</motion.div>
<AnimatePresence mode="wait">
{calculationType === 'custom' ? ( {calculationType === 'custom' ? (
<div className="space-y-4"> <motion.div
key="custom"
className="space-y-4"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Select Currency Select Currency
@ -153,18 +211,32 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
</div> </div>
{customAmount && Number(customAmount) > 0 && ( {customAmount && Number(customAmount) > 0 && (
<button <motion.button
onClick={() => onOffsetClick?.(0, Number(customAmount))} onClick={() => onOffsetClick?.(0, Number(customAmount))}
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors mt-6" className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors mt-6"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
> >
Offset Your Impact Offset Your Impact
</button> </motion.button>
)} )}
</div> </motion.div>
) : ( ) : (
<form onSubmit={handleCalculate} className="space-y-4"> <motion.form
key="calculator"
onSubmit={handleCalculate}
className="space-y-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{calculationType === 'fuel' && ( {calculationType === 'fuel' && (
<div> <motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
Fuel Consumption Fuel Consumption
</label> </label>
@ -191,12 +263,16 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
</select> </select>
</div> </div>
</div> </div>
</div> </motion.div>
)} )}
{calculationType === 'distance' && ( {calculationType === 'distance' && (
<> <>
<div> <motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
Distance (nautical miles) Distance (nautical miles)
</label> </label>
@ -208,9 +284,13 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
required required
/> />
</div> </motion.div>
<div> <motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
Average Speed (knots) Average Speed (knots)
</label> </label>
@ -223,9 +303,13 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
required required
/> />
</div> </motion.div>
<div> <motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3 }}
>
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
Fuel Consumption Rate (liters per hour) Fuel Consumption Rate (liters per hour)
</label> </label>
@ -241,54 +325,96 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
Typical range: 50 - 500 liters per hour for most yachts Typical range: 50 - 500 liters per hour for most yachts
</p> </p>
</div> </motion.div>
</> </>
)} )}
<div> <motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.4 }}
>
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
Select Currency Select Currency
</label> </label>
<div className="max-w-xs"> <div className="max-w-xs">
<CurrencySelect value={currency} onChange={setCurrency} /> <CurrencySelect value={currency} onChange={setCurrency} />
</div> </div>
</div> </motion.div>
<button <motion.button
type="submit" type="submit"
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors" className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay: 0.5,
type: "spring",
stiffness: 400,
damping: 17
}}
> >
Calculate Impact Calculate Impact
</button> </motion.button>
</form> </motion.form>
)} )}
</AnimatePresence>
<AnimatePresence>
{tripEstimate && calculationType !== 'custom' && ( {tripEstimate && calculationType !== 'custom' && (
<div className="mt-6 space-y-6"> <motion.div
<div className="grid grid-cols-2 gap-4"> className="mt-6 space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<motion.div
className="grid grid-cols-2 gap-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
{calculationType === 'distance' && ( {calculationType === 'distance' && (
<div className="bg-gray-50 p-4 rounded-lg"> <motion.div
className="bg-gray-50 p-4 rounded-lg"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<p className="text-sm text-gray-600">Trip Duration</p> <p className="text-sm text-gray-600">Trip Duration</p>
<p className="text-xl font-bold text-gray-900"> <p className="text-xl font-bold text-gray-900">
{tripEstimate.duration.toFixed(1)} hours {tripEstimate.duration.toFixed(1)} hours
</p> </p>
</div> </motion.div>
)} )}
<div className="bg-gray-50 p-4 rounded-lg"> <motion.div
className="bg-gray-50 p-4 rounded-lg"
initial={{ opacity: 0, x: calculationType === 'distance' ? 20 : 0 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<p className="text-sm text-gray-600">Fuel Consumption</p> <p className="text-sm text-gray-600">Fuel Consumption</p>
<p className="text-xl font-bold text-gray-900"> <p className="text-xl font-bold text-gray-900">
{tripEstimate.fuelConsumption.toLocaleString()} {fuelUnit} {tripEstimate.fuelConsumption.toLocaleString()} {fuelUnit}
</p> </p>
</div> </motion.div>
</div> </motion.div>
<div> <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.5 }}
>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Offset Percentage Offset Percentage
</label> </label>
<div className="flex flex-wrap gap-3 mb-3"> <div className="flex flex-wrap gap-3 mb-3">
{[100, 75, 50, 25].map((percent) => ( {[100, 75, 50, 25].map((percent, index) => (
<button <motion.button
key={percent} key={percent}
type="button" type="button"
onClick={() => handlePresetPercentage(percent)} onClick={() => handlePresetPercentage(percent)}
@ -297,11 +423,27 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`} }`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay: 0.6 + (index * 0.1),
type: "spring",
stiffness: 400,
damping: 17
}}
> >
{percent}% {percent}%
</button> </motion.button>
))} ))}
<div className="flex items-center space-x-2"> <motion.div
className="flex items-center space-x-2"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 1.0 }}
>
<input <input
type="number" type="number"
value={customPercentage} value={customPercentage}
@ -312,11 +454,16 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
className="w-24 px-3 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500" className="w-24 px-3 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500"
/> />
<span className="text-gray-600">%</span> <span className="text-gray-600">%</span>
</motion.div>
</div> </div>
</div> </motion.div>
</div>
<div className="bg-blue-50 p-4 rounded-lg"> <motion.div
className="bg-blue-50 p-4 rounded-lg"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 1.1 }}
>
<p className="text-sm text-gray-600">Selected CO Offset</p> <p className="text-sm text-gray-600">Selected CO Offset</p>
<p className="text-2xl font-bold text-blue-900"> <p className="text-2xl font-bold text-blue-900">
{calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage).toFixed(2)} tons {calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage).toFixed(2)} tons
@ -324,16 +471,28 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
<p className="text-sm text-blue-600 mt-1"> <p className="text-sm text-blue-600 mt-1">
{offsetPercentage}% of {tripEstimate.co2Emissions.toFixed(2)} tons {offsetPercentage}% of {tripEstimate.co2Emissions.toFixed(2)} tons
</p> </p>
</div> </motion.div>
<button <motion.button
onClick={() => onOffsetClick?.(calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage))} onClick={() => onOffsetClick?.(calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage))}
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors" className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.5,
delay: 1.2,
type: "spring",
stiffness: 400,
damping: 17
}}
> >
Offset Your Impact Offset Your Impact
</button> </motion.button>
</div> </motion.div>
)} )}
</div> </AnimatePresence>
</motion.div>
); );
} }