Add framer-motion animations to enhance UI interactions
- Install framer-motion dependency (v12.15.0) - Add smooth transitions to forms and buttons in TripCalculator - Implement hover and tap animations for interactive elements - Add entrance/exit animations for component state changes - Enhance user experience with motion effects in Home and OffsetOrder components
This commit is contained in:
parent
bf0f362ab7
commit
f9e4bc0149
@ -1,9 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Check, AlertCircle, ArrowLeft, Loader2, Globe2, TreePine, Waves, Factory, Wind, X } from 'lucide-react';
|
import { Check, AlertCircle, ArrowLeft, Loader2, Globe2, TreePine, Waves, Factory, Wind, X } from 'lucide-react';
|
||||||
<<<<<<< HEAD
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
=======
|
|
||||||
>>>>>>> 96496350ee59fb2fd226eb340094130203514ab8
|
|
||||||
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';
|
||||||
@ -60,9 +57,6 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
|||||||
message: `I would like to offset ${tons.toFixed(2)} tons of CO2 from my yacht's ${calculatorType} emissions.`
|
message: `I would like to offset ${tons.toFixed(2)} tons of CO2 from my yacht's ${calculatorType} emissions.`
|
||||||
});
|
});
|
||||||
|
|
||||||
// lightbox state
|
|
||||||
const [selectedProject, setSelectedProject] = useState<OffsetProject | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config.wrenApiKey) {
|
if (!config.wrenApiKey) {
|
||||||
setError('Carbon offset service is currently unavailable. Please use our contact form to request offsetting.');
|
setError('Carbon offset service is currently unavailable. Please use our contact form to request offsetting.');
|
||||||
@ -72,8 +66,8 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr
|
|||||||
fetchPortfolio();
|
fetchPortfolio();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// handle Escape key to close lightbox
|
// handle Escape key to close lightbox
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') setSelectedProject(null);
|
if (e.key === 'Escape') setSelectedProject(null);
|
||||||
};
|
};
|
||||||
@ -83,7 +77,7 @@ useEffect(() => {
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onKey);
|
document.removeEventListener('keydown', onKey);
|
||||||
};
|
};
|
||||||
}, [selectedProject]);
|
}, [selectedProject]);
|
||||||
|
|
||||||
const fetchPortfolio = async () => {
|
const fetchPortfolio = async () => {
|
||||||
try {
|
try {
|
||||||
@ -341,7 +335,6 @@ useEffect(() => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{portfolio.projects && portfolio.projects.length > 0 && (
|
{portfolio.projects && portfolio.projects.length > 0 && (
|
||||||
<<<<<<< HEAD
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@ -357,17 +350,11 @@ useEffect(() => {
|
|||||||
transition={{ duration: 0.5, delay: 0.1 * index }}
|
transition={{ duration: 0.5, delay: 0.1 * index }}
|
||||||
whileHover={{ scale: 1.03 }}
|
whileHover={{ scale: 1.03 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setSelectedProject(project)}
|
onClick={() => {
|
||||||
|
console.log('Project clicked:', project.name);
|
||||||
|
setSelectedProject(project);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
=======
|
|
||||||
<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}
|
|
||||||
onClick={() => setSelectedProject(project)}
|
|
||||||
className="cursor-pointer bg-gray-50 rounded-lg p-4 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
>>>>>>> 96496350ee59fb2fd226eb340094130203514ab8
|
|
||||||
<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} />
|
||||||
@ -407,56 +394,12 @@ useEffect(() => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex items-center justify-between bg-blue-50 p-4 rounded-lg"
|
className="flex items-center justify-between bg-blue-50 p-4 rounded-lg"
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 0.5 }}
|
transition={{ duration: 0.5, delay: 0.5 }}
|
||||||
>
|
>
|
||||||
=======
|
|
||||||
{/* lightbox modal */}
|
|
||||||
{selectedProject && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
|
||||||
onClick={() => setSelectedProject(null)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="relative bg-white rounded-lg p-6 max-w-lg w-full"
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedProject(null)}
|
|
||||||
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
|
|
||||||
>
|
|
||||||
<X size={24} />
|
|
||||||
</button>
|
|
||||||
<h3 className="text-2xl font-bold mb-4">
|
|
||||||
{selectedProject.name}
|
|
||||||
</h3>
|
|
||||||
{selectedProject.imageUrl && (
|
|
||||||
<img
|
|
||||||
src={selectedProject.imageUrl}
|
|
||||||
alt={selectedProject.name}
|
|
||||||
className="w-full h-48 object-cover rounded mb-4"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
{selectedProject.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="font-medium">Type:</span>
|
|
||||||
<span>{selectedProject.type}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-2">
|
|
||||||
<span className="font-medium">Price per ton:</span>
|
|
||||||
<span>${selectedProject.pricePerTon.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center justify-between bg-blue-50 p-4 rounded-lg">
|
|
||||||
>>>>>>> 96496350ee59fb2fd226eb340094130203514ab8
|
|
||||||
<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)}
|
||||||
@ -539,7 +482,7 @@ useEffect(() => {
|
|||||||
|
|
||||||
{/* Modal Content */}
|
{/* Modal Content */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative bg-white rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
className="relative bg-white rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-60"
|
||||||
initial={{ scale: 0.9, opacity: 0 }}
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
exit={{ scale: 0.9, opacity: 0 }}
|
exit={{ scale: 0.9, opacity: 0 }}
|
||||||
@ -549,7 +492,7 @@ useEffect(() => {
|
|||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedProject(null)}
|
onClick={() => setSelectedProject(null)}
|
||||||
className="absolute top-4 right-4 p-2 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
|
className="absolute top-4 right-4 p-2 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user