Added lightboxes
This commit is contained in:
parent
5d0cfdef47
commit
2376205371
49
package-lock.json
generated
49
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
20
src/App.tsx
20
src/App.tsx
@ -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">
|
||||||
{renderPage()}
|
<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()}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="bg-white mt-16">
|
<footer className="bg-white mt-16">
|
||||||
|
|||||||
@ -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.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
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">
|
||||||
|
<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} />
|
||||||
|
</motion.div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">Flexible Offsetting Solutions</h2>
|
||||||
|
</div>
|
||||||
|
<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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</motion.div>
|
||||||
</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="bg-white rounded-xl shadow-lg p-8"
|
||||||
<div className="flex items-center space-x-4 mb-6">
|
variants={fadeInUp}
|
||||||
<Globe className="text-blue-600" size={32} />
|
initial="rest"
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Flexible Offsetting Solutions</h2>
|
whileInView="rest"
|
||||||
</div>
|
viewport={{ once: true }}
|
||||||
<p className="text-gray-600 leading-relaxed text-justify">
|
whileHover="hover"
|
||||||
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>
|
<motion.div variants={scaleOnHover} className="h-full">
|
||||||
</div>
|
<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} />
|
||||||
|
</motion.div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">Your Values, Your Choice</h2>
|
||||||
|
</div>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
</motion.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
|
||||||
<div className="flex items-center space-x-4 mb-6">
|
className="bg-white rounded-xl shadow-lg p-12 mb-16 text-center"
|
||||||
<BarChart className="text-green-600" size={32} />
|
initial={{ opacity: 0, y: 40 }}
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Your Values, Your Choice</h2>
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
</div>
|
transition={{ duration: 0.7, ease: "easeOut" }}
|
||||||
<p className="text-gray-600 leading-relaxed text-justify">
|
viewport={{ once: true, amount: 0.3 }}
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-lg p-12 mb-16 text-center">
|
|
||||||
<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">
|
||||||
<Anchor className="text-blue-600" size={32} />
|
<motion.div
|
||||||
<h2 className="text-3xl font-bold text-gray-900">
|
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} />
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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,35 +370,48 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-3 text-center">
|
||||||
|
<span className="text-xs text-blue-600 font-medium">Click for details</span>
|
||||||
|
</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">
|
||||||
<span className="text-gray-600">Amount to Offset:</span>
|
<span className="text-gray-600">Amount to Offset:</span>
|
||||||
<span className="font-medium">{tons.toFixed(2)} tons CO₂</span>
|
<span className="font-medium">{tons.toFixed(2)} tons CO₂</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Portfolio Distribution:</span>
|
<span className="text-gray-600">Portfolio Distribution:</span>
|
||||||
<span className="font-medium">Automatically optimized</span>
|
<span className="font-medium">Automatically optimized</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Cost per Ton:</span>
|
<span className="text-gray-600">Cost per Ton:</span>
|
||||||
<span className="font-medium">{renderPortfolioPrice(portfolio)}</span>
|
<span className="font-medium">{renderPortfolioPrice(portfolio)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-900 font-semibold">Total Cost:</span>
|
<span className="text-gray-900 font-semibold">Total Cost:</span>
|
||||||
<span className="text-gray-900 font-semibold">
|
<span className="text-gray-900 font-semibold">
|
||||||
@ -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}
|
||||||
</div>
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
<Route className="text-blue-500" size={24} />
|
<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} />
|
||||||
|
</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,228 +159,340 @@ 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>
|
||||||
|
|
||||||
{calculationType === 'custom' ? (
|
<AnimatePresence mode="wait">
|
||||||
<div className="space-y-4">
|
{calculationType === 'custom' ? (
|
||||||
<div>
|
<motion.div
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
key="custom"
|
||||||
Select Currency
|
className="space-y-4"
|
||||||
</label>
|
initial={{ opacity: 0, height: 0 }}
|
||||||
<div className="max-w-xs">
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
<CurrencySelect value={currency} onChange={setCurrency} />
|
exit={{ opacity: 0, height: 0 }}
|
||||||
</div>
|
transition={{ duration: 0.3 }}
|
||||||
</div>
|
>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Enter Amount to Offset
|
|
||||||
</label>
|
|
||||||
<div className="relative rounded-md shadow-sm">
|
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
|
||||||
<span className="text-gray-500 sm:text-sm">{currencies[currency].symbol}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={customAmount}
|
|
||||||
onChange={handleCustomAmountChange}
|
|
||||||
placeholder="Enter amount"
|
|
||||||
min="0"
|
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 pl-7 pr-12 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
|
||||||
<span className="text-gray-500 sm:text-sm">{currency}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{customAmount && Number(customAmount) > 0 && (
|
|
||||||
<button
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
Offset Your Impact
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleCalculate} className="space-y-4">
|
|
||||||
{calculationType === 'fuel' && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Fuel Consumption
|
Select Currency
|
||||||
</label>
|
</label>
|
||||||
<div className="flex space-x-4">
|
<div className="max-w-xs">
|
||||||
<div className="flex-1">
|
<CurrencySelect value={currency} onChange={setCurrency} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Enter Amount to Offset
|
||||||
|
</label>
|
||||||
|
<div className="relative rounded-md shadow-sm">
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<span className="text-gray-500 sm:text-sm">{currencies[currency].symbol}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={customAmount}
|
||||||
|
onChange={handleCustomAmountChange}
|
||||||
|
placeholder="Enter amount"
|
||||||
|
min="0"
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 pl-7 pr-12 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
|
<span className="text-gray-500 sm:text-sm">{currency}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{customAmount && Number(customAmount) > 0 && (
|
||||||
|
<motion.button
|
||||||
|
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"
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
>
|
||||||
|
Offset Your Impact
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<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' && (
|
||||||
|
<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">
|
||||||
|
Fuel Consumption
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={fuelAmount}
|
||||||
|
onChange={(e) => setFuelAmount(e.target.value)}
|
||||||
|
placeholder="Enter amount"
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={fuelUnit}
|
||||||
|
onChange={(e) => setFuelUnit(e.target.value as 'liters' | 'gallons')}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||||||
|
>
|
||||||
|
<option value="liters">Liters</option>
|
||||||
|
<option value="gallons">Gallons</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{calculationType === 'distance' && (
|
||||||
|
<>
|
||||||
|
<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">
|
||||||
|
Distance (nautical miles)
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
value={fuelAmount}
|
value={distance}
|
||||||
onChange={(e) => setFuelAmount(e.target.value)}
|
onChange={(e) => setDistance(e.target.value)}
|
||||||
placeholder="Enter amount"
|
|
||||||
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>
|
|
||||||
<select
|
|
||||||
value={fuelUnit}
|
|
||||||
onChange={(e) => setFuelUnit(e.target.value as 'liters' | 'gallons')}
|
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
|
||||||
>
|
|
||||||
<option value="liters">Liters</option>
|
|
||||||
<option value="gallons">Gallons</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{calculationType === 'distance' && (
|
<motion.div
|
||||||
<>
|
initial={{ opacity: 0, y: 10 }}
|
||||||
<div>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
transition={{ duration: 0.3, delay: 0.2 }}
|
||||||
Distance (nautical miles)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={distance}
|
|
||||||
onChange={(e) => setDistance(e.target.value)}
|
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
|
||||||
Average Speed (knots)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="50"
|
|
||||||
value={speed}
|
|
||||||
onChange={(e) => setSpeed(e.target.value)}
|
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
|
||||||
Fuel Consumption Rate (liters per hour)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
value={fuelRate}
|
|
||||||
onChange={(e) => setFuelRate(e.target.value)}
|
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Typical range: 50 - 500 liters per hour for most yachts
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
|
||||||
Select Currency
|
|
||||||
</label>
|
|
||||||
<div className="max-w-xs">
|
|
||||||
<CurrencySelect value={currency} onChange={setCurrency} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
|
|
||||||
>
|
|
||||||
Calculate Impact
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tripEstimate && calculationType !== 'custom' && (
|
|
||||||
<div className="mt-6 space-y-6">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{calculationType === 'distance' && (
|
|
||||||
<div className="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<p className="text-sm text-gray-600">Trip Duration</p>
|
|
||||||
<p className="text-xl font-bold text-gray-900">
|
|
||||||
{tripEstimate.duration.toFixed(1)} hours
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<p className="text-sm text-gray-600">Fuel Consumption</p>
|
|
||||||
<p className="text-xl font-bold text-gray-900">
|
|
||||||
{tripEstimate.fuelConsumption.toLocaleString()} {fuelUnit}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Offset Percentage
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-3 mb-3">
|
|
||||||
{[100, 75, 50, 25].map((percent) => (
|
|
||||||
<button
|
|
||||||
key={percent}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handlePresetPercentage(percent)}
|
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
|
||||||
offsetPercentage === percent && customPercentage === ''
|
|
||||||
? 'bg-blue-500 text-white'
|
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{percent}%
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
</button>
|
Average Speed (knots)
|
||||||
))}
|
</label>
|
||||||
<div className="flex items-center space-x-2">
|
<input
|
||||||
<input
|
type="number"
|
||||||
type="number"
|
min="1"
|
||||||
value={customPercentage}
|
max="50"
|
||||||
onChange={handleCustomPercentageChange}
|
value={speed}
|
||||||
placeholder="Custom %"
|
onChange={(e) => setSpeed(e.target.value)}
|
||||||
min="0"
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||||||
max="100"
|
required
|
||||||
className="w-24 px-3 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
/>
|
||||||
/>
|
</motion.div>
|
||||||
<span className="text-gray-600">%</span>
|
|
||||||
|
<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">
|
||||||
|
Fuel Consumption Rate (liters per hour)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
value={fuelRate}
|
||||||
|
onChange={(e) => setFuelRate(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Typical range: 50 - 500 liters per hour for most yachts
|
||||||
|
</p>
|
||||||
|
</motion.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">
|
||||||
|
Select Currency
|
||||||
|
</label>
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<CurrencySelect value={currency} onChange={setCurrency} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg">
|
<motion.button
|
||||||
<p className="text-sm text-gray-600">Selected CO₂ Offset</p>
|
type="submit"
|
||||||
<p className="text-2xl font-bold text-blue-900">
|
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
{calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage).toFixed(2)} tons
|
whileHover={{ scale: 1.03 }}
|
||||||
</p>
|
whileTap={{ scale: 0.97 }}
|
||||||
<p className="text-sm text-blue-600 mt-1">
|
initial={{ opacity: 0, y: 10 }}
|
||||||
{offsetPercentage}% of {tripEstimate.co2Emissions.toFixed(2)} tons
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</p>
|
transition={{
|
||||||
</div>
|
duration: 0.3,
|
||||||
|
delay: 0.5,
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 17
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Calculate Impact
|
||||||
|
</motion.button>
|
||||||
|
</motion.form>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<button
|
<AnimatePresence>
|
||||||
onClick={() => onOffsetClick?.(calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage))}
|
{tripEstimate && calculationType !== 'custom' && (
|
||||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
|
<motion.div
|
||||||
|
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" }}
|
||||||
>
|
>
|
||||||
Offset Your Impact
|
<motion.div
|
||||||
</button>
|
className="grid grid-cols-2 gap-4"
|
||||||
</div>
|
initial={{ opacity: 0 }}
|
||||||
)}
|
animate={{ opacity: 1 }}
|
||||||
</div>
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
{calculationType === 'distance' && (
|
||||||
|
<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-xl font-bold text-gray-900">
|
||||||
|
{tripEstimate.duration.toFixed(1)} hours
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
<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-xl font-bold text-gray-900">
|
||||||
|
{tripEstimate.fuelConsumption.toLocaleString()} {fuelUnit}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.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">
|
||||||
|
Offset Percentage
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-3 mb-3">
|
||||||
|
{[100, 75, 50, 25].map((percent, index) => (
|
||||||
|
<motion.button
|
||||||
|
key={percent}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePresetPercentage(percent)}
|
||||||
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
|
offsetPercentage === percent && customPercentage === ''
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: '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}%
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
<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
|
||||||
|
type="number"
|
||||||
|
value={customPercentage}
|
||||||
|
onChange={handleCustomPercentageChange}
|
||||||
|
placeholder="Custom %"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
className="w-24 px-3 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-gray-600">%</span>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<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-2xl font-bold text-blue-900">
|
||||||
|
{calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage).toFixed(2)} tons
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-blue-600 mt-1">
|
||||||
|
{offsetPercentage}% of {tripEstimate.co2Emissions.toFixed(2)} tons
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
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"
|
||||||
|
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
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user