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": {
|
||||
"axios": "^1.6.7",
|
||||
"dotenv": "^8.2.0",
|
||||
"framer-motion": "^12.15.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
@ -3431,6 +3432,33 @@
|
||||
"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": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -4500,6 +4528,21 @@
|
||||
"integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
|
||||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -5928,6 +5971,12 @@
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"dotenv": "^8.2.0",
|
||||
"framer-motion": "^12.15.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
@ -37,4 +38,4 @@
|
||||
"vite": "^5.4.2",
|
||||
"vitest": "^1.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
src/App.tsx
22
src/App.tsx
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Bird, Menu, X } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Home } from './components/Home';
|
||||
import { YachtSearch } from './components/YachtSearch';
|
||||
import { TripCalculator } from './components/TripCalculator';
|
||||
@ -8,7 +9,7 @@ import { About } from './components/About';
|
||||
import { Contact } from './components/Contact';
|
||||
import { OffsetOrder } from './components/OffsetOrder';
|
||||
import { getVesselData } from './api/aisClient';
|
||||
import { calculateCarbon } from './utils/carbonCalculator';
|
||||
import { calculateTripCarbon } from './utils/carbonCalculator';
|
||||
import { analytics } from './utils/analytics';
|
||||
import type { VesselData, CarbonCalculation, CalculatorType } from './types';
|
||||
|
||||
@ -211,8 +212,21 @@ function App() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto py-8 sm:py-12">
|
||||
{renderPage()}
|
||||
<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()}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
|
||||
<footer className="bg-white mt-16">
|
||||
@ -226,4 +240,4 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Anchor, Globe, BarChart } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface Props {
|
||||
onNavigate: (page: 'home' | 'calculator' | 'how-it-works' | 'about' | 'contact') => void;
|
||||
@ -20,87 +21,252 @@ export function Home({ onNavigate }: Props) {
|
||||
}, 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 (
|
||||
<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]">
|
||||
<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"
|
||||
alt="Luxury yacht on calm waters"
|
||||
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 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">
|
||||
<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
|
||||
</h1>
|
||||
<p className="text-xl text-white max-w-3xl mx-auto leading-relaxed drop-shadow-lg">
|
||||
</motion.h1>
|
||||
<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.
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 transform hover:scale-105 transition-transform duration-300">
|
||||
<div className="flex items-center space-x-4 mb-6">
|
||||
<Globe className="text-blue-600" size={32} />
|
||||
<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>
|
||||
</div>
|
||||
<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.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">
|
||||
<div className="flex items-center space-x-4 mb-6">
|
||||
<BarChart className="text-green-600" size={32} />
|
||||
<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>
|
||||
</div>
|
||||
</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="flex items-center justify-center space-x-4 mb-6">
|
||||
<Anchor className="text-blue-600" size={32} />
|
||||
<h2 className="text-3xl font-bold text-gray-900">
|
||||
<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} />
|
||||
</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
|
||||
</h2>
|
||||
</motion.h2>
|
||||
</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.
|
||||
</p>
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="text-center bg-white rounded-xl shadow-lg p-12 mb-16">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-6">
|
||||
<motion.div
|
||||
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?
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
|
||||
</motion.h2>
|
||||
<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.
|
||||
</p>
|
||||
<div className="flex justify-center space-x-4">
|
||||
<button
|
||||
</motion.p>
|
||||
<motion.div
|
||||
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}
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
</motion.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}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
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 type { CurrencyCode, OffsetOrder as OffsetOrderType, Portfolio, OffsetProject } from '../types';
|
||||
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 [portfolio, setPortfolio] = useState<Portfolio | null>(null);
|
||||
const [loadingPortfolio, setLoadingPortfolio] = useState(true);
|
||||
const [selectedProject, setSelectedProject] = useState<OffsetProject | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
@ -127,23 +129,37 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
||||
const offsetCost = monetaryAmount || (portfolio ? tons * (portfolio.pricePerTon || 18) : 0);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl p-8 max-w-4xl w-full">
|
||||
<button
|
||||
<motion.div
|
||||
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}
|
||||
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} />
|
||||
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">
|
||||
Offset Your Impact
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
You're about to offset {tons.toFixed(2)} tons of CO₂
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{error && !config.wrenApiKey ? (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
@ -306,9 +322,23 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
||||
</p>
|
||||
|
||||
{portfolio.projects && portfolio.projects.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
||||
{portfolio.projects.map((project) => (
|
||||
<div key={project.id} className="bg-gray-50 rounded-lg p-4 hover:shadow-md transition-shadow">
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"
|
||||
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 space-x-2">
|
||||
<ProjectTypeIcon project={project} />
|
||||
@ -325,7 +355,7 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
||||
<img
|
||||
src={project.imageUrl}
|
||||
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>
|
||||
)}
|
||||
@ -340,35 +370,48 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
||||
</span>
|
||||
</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-bold text-lg">
|
||||
{renderPortfolioPrice(portfolio)}
|
||||
</span>
|
||||
</div>
|
||||
</motion.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>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Amount to Offset:</span>
|
||||
<span className="font-medium">{tons.toFixed(2)} tons CO₂</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Portfolio Distribution:</span>
|
||||
<span className="font-medium">Automatically optimized</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Cost per Ton:</span>
|
||||
<span className="font-medium">{renderPortfolioPrice(portfolio)}</span>
|
||||
</div>
|
||||
<div className="border-t pt-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Amount to Offset:</span>
|
||||
<span className="font-medium">{tons.toFixed(2)} tons CO₂</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Portfolio Distribution:</span>
|
||||
<span className="font-medium">Automatically optimized</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Cost per Ton:</span>
|
||||
<span className="font-medium">{renderPortfolioPrice(portfolio)}</span>
|
||||
</div>
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-900 font-semibold">Total Cost:</span>
|
||||
<span className="text-gray-900 font-semibold">
|
||||
@ -377,14 +420,19 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<button
|
||||
<motion.button
|
||||
onClick={handleOffsetOrder}
|
||||
disabled={loading}
|
||||
className={`w-full bg-blue-500 text-white py-3 px-4 rounded-lg transition-colors ${
|
||||
loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-600'
|
||||
}`}
|
||||
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 ? (
|
||||
<div className="flex items-center justify-center">
|
||||
@ -394,9 +442,123 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
||||
) : (
|
||||
'Confirm Offset Order'
|
||||
)}
|
||||
</button>
|
||||
</motion.button>
|
||||
</>
|
||||
) : 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 { Route } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import type { VesselData, TripEstimate, CurrencyCode } from '../types';
|
||||
import { calculateTripCarbon, calculateCarbonFromFuel } from '../utils/carbonCalculator';
|
||||
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 (
|
||||
<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">
|
||||
<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 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">
|
||||
Calculation Method
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={() => setCalculationType('fuel')}
|
||||
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-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
|
||||
</button>
|
||||
<button
|
||||
</motion.button>
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={() => setCalculationType('distance')}
|
||||
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-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
|
||||
</button>
|
||||
<button
|
||||
</motion.button>
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={() => setCalculationType('custom')}
|
||||
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-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
|
||||
</button>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{calculationType === 'custom' ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Currency
|
||||
</label>
|
||||
<div className="max-w-xs">
|
||||
<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 && (
|
||||
<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' && (
|
||||
<AnimatePresence mode="wait">
|
||||
{calculationType === 'custom' ? (
|
||||
<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>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Fuel Consumption
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Currency
|
||||
</label>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<div className="max-w-xs">
|
||||
<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
|
||||
type="number"
|
||||
min="1"
|
||||
value={fuelAmount}
|
||||
onChange={(e) => setFuelAmount(e.target.value)}
|
||||
placeholder="Enter amount"
|
||||
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>
|
||||
<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>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{calculationType === 'distance' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
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'
|
||||
}`}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
{percent}%
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center space-x-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<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>
|
||||
</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
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<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"
|
||||
<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>
|
||||
</motion.div>
|
||||
|
||||
<motion.button
|
||||
type="submit"
|
||||
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
|
||||
</motion.button>
|
||||
</motion.form>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{tripEstimate && calculationType !== 'custom' && (
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<motion.div
|
||||
className="grid grid-cols-2 gap-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
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