puffin-app/src/components/CarbonImpactComparison.tsx

177 lines
5.0 KiB
TypeScript
Raw Normal View History

/**
* CarbonImpactComparison Component
*
* Displays varied carbon offset impact comparisons with animations.
* Uses EPA/DEFRA/IMO 2024 verified conversion factors.
*/
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import * as LucideIcons from 'lucide-react';
import { selectComparisons } from '../utils/impactSelector';
import type { CarbonComparison } from '../types/carbonEquivalencies';
interface CarbonImpactComparisonProps {
/** Metric tons of CO2e being offset */
tons: number;
/** Display variant */
variant?: 'preview' | 'success';
/** Number of comparisons to show (default: 3) */
count?: number;
/** Additional CSS classes */
className?: string;
}
/**
* Animated counter component with count-up effect
*/
interface AnimatedCounterProps {
value: number;
duration?: number;
}
function AnimatedCounter({ value, duration = 2 }: AnimatedCounterProps) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.5 });
return (
<motion.span
ref={ref}
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 0.3 }}
>
{isInView && (
<motion.span
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 100, damping: 10 }}
>
{value.toLocaleString()}
</motion.span>
)}
</motion.span>
);
}
/**
* Get Lucide icon component by name
*/
function getLucideIcon(iconName: string): React.ElementType {
const Icon = (LucideIcons as Record<string, React.ElementType>)[iconName];
return Icon || LucideIcons.Leaf; // Fallback to Leaf icon
}
/**
* Individual comparison card component
*/
interface ComparisonCardProps {
comparison: CarbonComparison;
index: number;
}
function ComparisonCard({ comparison, index }: ComparisonCardProps) {
const Icon = getLucideIcon(comparison.icon);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.5 }}
className="bg-white/10 backdrop-blur-md rounded-xl p-6 border border-white/20 hover:border-emerald-400/50 transition-all duration-300 hover:shadow-lg hover:shadow-emerald-500/20"
>
{/* Icon */}
<div className="mb-4 flex items-center justify-center">
<div className="bg-emerald-500/20 p-3 rounded-full">
<Icon className="w-8 h-8 text-emerald-400" />
</div>
</div>
{/* Value */}
<div className="text-center mb-2">
<div className="text-3xl font-bold text-white">
<AnimatedCounter value={comparison.value} />
</div>
<div className="text-sm text-emerald-300 mt-1">{comparison.unit}</div>
</div>
{/* Label */}
<div className="text-center">
<p className="text-sm text-white/90 font-medium">{comparison.label}</p>
{comparison.description && (
<p className="text-xs text-white/60 mt-2">{comparison.description}</p>
)}
</div>
{/* Source */}
<div className="text-center mt-4">
<span className="text-xs text-white/50 italic">Source: {comparison.source}</span>
</div>
</motion.div>
);
}
/**
* Main CarbonImpactComparison component
*/
export function CarbonImpactComparison({
tons,
variant = 'preview',
count = 3,
className = '',
}: CarbonImpactComparisonProps) {
// Get appropriate comparisons for this CO2 amount
const comparisons = selectComparisons(tons, count);
// Variant-specific styling
const titleText = variant === 'success' ? 'Your Impact' : 'Making an Impact';
const subtitleText =
variant === 'success'
? "Here's what your offset is equivalent to:"
: 'Your carbon offset is equivalent to:';
return (
<div className={`w-full ${className}`}>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center mb-8"
>
<h3 className="text-2xl md:text-3xl font-bold text-white mb-2">{titleText}</h3>
<p className="text-white/80 text-sm md:text-base">{subtitleText}</p>
</motion.div>
{/* Comparison Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{comparisons.map((comparison, index) => (
<ComparisonCard
key={`${comparison.category}-${comparison.label}`}
comparison={comparison}
index={index}
/>
))}
</div>
{/* Footer Note */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.5 }}
className="text-center mt-6"
>
<p className="text-xs text-white/50">
Equivalencies calculated using EPA 2024, DEFRA 2024, and IMO 2024 verified conversion
factors.
</p>
</motion.div>
</div>
);
}
export default CarbonImpactComparison;