Transport components: - TransportTimeline: Visual timeline of transport events with status badges - JourneyMap: SVG-based map visualization of plant journey locations - CarbonFootprintCard: Carbon metrics display with comparison charts - QRCodeDisplay: QR code generation for traceability verification - TransportEventForm: Form for recording transport events Demand components: - DemandSignalCard: Regional demand signal with supply status indicators - PreferencesForm: Multi-section consumer preference input form - RecommendationList: Planting recommendations with risk assessment - SupplyGapChart: Supply vs demand visualization with gap indicators - SeasonalCalendar: Seasonal produce availability calendar view Analytics components: - EnvironmentalImpact: Comprehensive carbon and food miles analysis - FoodMilesTracker: Food miles tracking with daily charts and targets - SavingsCalculator: Environmental savings vs conventional agriculture All components follow existing patterns, use Tailwind CSS, and are fully typed.
273 lines
12 KiB
TypeScript
273 lines
12 KiB
TypeScript
import { useState, useMemo } from 'react';
|
||
|
||
interface SavingsCalculatorProps {
|
||
actualCarbon: number;
|
||
actualMiles: number;
|
||
produceWeightKg: number;
|
||
conventionalData?: ConventionalBaseline;
|
||
}
|
||
|
||
export interface ConventionalBaseline {
|
||
avgCarbonPerKg: number; // Default: 2.5 kg CO2 per kg produce
|
||
avgMilesPerKg: number; // Default: 2400 km average food miles
|
||
avgWaterLitersPerKg: number; // Default: 500 liters per kg
|
||
avgFoodWastePercent: number; // Default: 30% waste
|
||
}
|
||
|
||
const DEFAULT_CONVENTIONAL: ConventionalBaseline = {
|
||
avgCarbonPerKg: 2.5,
|
||
avgMilesPerKg: 2400,
|
||
avgWaterLitersPerKg: 500,
|
||
avgFoodWastePercent: 30,
|
||
};
|
||
|
||
const EQUIVALENTS = {
|
||
carbonPerTree: 21, // kg CO2 absorbed per tree per year
|
||
carbonPerCarMile: 0.404, // kg CO2 per mile driven
|
||
carbonPerFlight: 250, // kg CO2 per hour of flight
|
||
waterPerShower: 75, // liters per 5-min shower
|
||
carbonPerGallon: 8.89, // kg CO2 per gallon of gasoline
|
||
};
|
||
|
||
export default function SavingsCalculator({
|
||
actualCarbon,
|
||
actualMiles,
|
||
produceWeightKg,
|
||
conventionalData = DEFAULT_CONVENTIONAL,
|
||
}: SavingsCalculatorProps) {
|
||
const [showDetails, setShowDetails] = useState(false);
|
||
|
||
// Calculate conventional equivalents
|
||
const conventionalCarbon = produceWeightKg * conventionalData.avgCarbonPerKg;
|
||
const conventionalMiles = produceWeightKg * conventionalData.avgMilesPerKg;
|
||
const conventionalWater = produceWeightKg * conventionalData.avgWaterLitersPerKg;
|
||
const conventionalWaste = produceWeightKg * (conventionalData.avgFoodWastePercent / 100);
|
||
|
||
// Calculate savings
|
||
const carbonSaved = conventionalCarbon - actualCarbon;
|
||
const milesSaved = conventionalMiles - actualMiles;
|
||
const wasteSaved = conventionalWaste * 0.75; // Assume 75% less waste with local
|
||
|
||
// Calculate percentage reductions
|
||
const carbonReduction = conventionalCarbon > 0 ? (carbonSaved / conventionalCarbon) * 100 : 0;
|
||
const milesReduction = conventionalMiles > 0 ? (milesSaved / conventionalMiles) * 100 : 0;
|
||
|
||
// Convert to tangible equivalents
|
||
const equivalents = useMemo(() => ({
|
||
treesEquivalent: carbonSaved / EQUIVALENTS.carbonPerTree,
|
||
carMilesEquivalent: carbonSaved / EQUIVALENTS.carbonPerCarMile,
|
||
flightHoursEquivalent: carbonSaved / EQUIVALENTS.carbonPerFlight,
|
||
showersEquivalent: (conventionalWater - conventionalWater * 0.1) / EQUIVALENTS.waterPerShower,
|
||
gallonsGasoline: carbonSaved / EQUIVALENTS.carbonPerGallon,
|
||
}), [carbonSaved, conventionalWater]);
|
||
|
||
// Calculate annual projections (assuming monthly data)
|
||
const annualMultiplier = 12;
|
||
const annualCarbonSaved = carbonSaved * annualMultiplier;
|
||
const annualMilesSaved = milesSaved * annualMultiplier;
|
||
|
||
// Rating based on carbon reduction
|
||
const getRating = (reduction: number): { stars: number; label: string; color: string } => {
|
||
if (reduction >= 90) return { stars: 5, label: 'Outstanding', color: 'text-green-600' };
|
||
if (reduction >= 75) return { stars: 4, label: 'Excellent', color: 'text-emerald-600' };
|
||
if (reduction >= 50) return { stars: 3, label: 'Great', color: 'text-lime-600' };
|
||
if (reduction >= 25) return { stars: 2, label: 'Good', color: 'text-yellow-600' };
|
||
return { stars: 1, label: 'Getting Started', color: 'text-orange-600' };
|
||
};
|
||
|
||
const rating = getRating(carbonReduction);
|
||
|
||
return (
|
||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||
{/* Header */}
|
||
<div className="bg-gradient-to-r from-green-600 to-emerald-600 text-white p-6">
|
||
<h3 className="text-xl font-bold">Savings Calculator</h3>
|
||
<p className="text-green-200 text-sm mt-1">See your environmental impact vs conventional</p>
|
||
</div>
|
||
|
||
<div className="p-6 space-y-6">
|
||
{/* Rating display */}
|
||
<div className="text-center p-6 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg">
|
||
<div className="flex justify-center gap-1 mb-2">
|
||
{[1, 2, 3, 4, 5].map((star) => (
|
||
<span
|
||
key={star}
|
||
className={`text-2xl ${star <= rating.stars ? 'text-yellow-400' : 'text-gray-300'}`}
|
||
>
|
||
⭐
|
||
</span>
|
||
))}
|
||
</div>
|
||
<p className={`text-xl font-bold ${rating.color}`}>{rating.label}</p>
|
||
<p className="text-sm text-gray-600 mt-1">
|
||
You've reduced carbon by {carbonReduction.toFixed(0)}% compared to conventional!
|
||
</p>
|
||
</div>
|
||
|
||
{/* Main savings metrics */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-4 bg-green-50 rounded-lg text-center">
|
||
<p className="text-3xl font-bold text-green-600">{carbonSaved.toFixed(1)}</p>
|
||
<p className="text-sm text-gray-500">kg CO2 Saved</p>
|
||
<div className="mt-2 h-2 bg-gray-200 rounded-full">
|
||
<div
|
||
className="h-full bg-green-500 rounded-full"
|
||
style={{ width: `${Math.min(carbonReduction, 100)}%` }}
|
||
></div>
|
||
</div>
|
||
<p className="text-xs text-gray-400 mt-1">{carbonReduction.toFixed(0)}% reduction</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-blue-50 rounded-lg text-center">
|
||
<p className="text-3xl font-bold text-blue-600">{milesSaved.toFixed(0)}</p>
|
||
<p className="text-sm text-gray-500">km Saved</p>
|
||
<div className="mt-2 h-2 bg-gray-200 rounded-full">
|
||
<div
|
||
className="h-full bg-blue-500 rounded-full"
|
||
style={{ width: `${Math.min(milesReduction, 100)}%` }}
|
||
></div>
|
||
</div>
|
||
<p className="text-xs text-gray-400 mt-1">{milesReduction.toFixed(0)}% reduction</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Comparison table */}
|
||
<div className="overflow-hidden border border-gray-200 rounded-lg">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50">
|
||
<tr>
|
||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-900">Metric</th>
|
||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">Your Impact</th>
|
||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-500">Conventional</th>
|
||
<th className="px-4 py-3 text-right text-sm font-semibold text-green-600">Saved</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-200">
|
||
<tr>
|
||
<td className="px-4 py-3 text-sm text-gray-600">Carbon Footprint</td>
|
||
<td className="px-4 py-3 text-sm text-right font-medium">{actualCarbon.toFixed(2)} kg</td>
|
||
<td className="px-4 py-3 text-sm text-right text-gray-500">{conventionalCarbon.toFixed(2)} kg</td>
|
||
<td className="px-4 py-3 text-sm text-right text-green-600 font-medium">
|
||
-{carbonSaved.toFixed(2)} kg
|
||
</td>
|
||
</tr>
|
||
<tr className="bg-gray-50">
|
||
<td className="px-4 py-3 text-sm text-gray-600">Food Miles</td>
|
||
<td className="px-4 py-3 text-sm text-right font-medium">{actualMiles.toFixed(0)} km</td>
|
||
<td className="px-4 py-3 text-sm text-right text-gray-500">{conventionalMiles.toFixed(0)} km</td>
|
||
<td className="px-4 py-3 text-sm text-right text-green-600 font-medium">
|
||
-{milesSaved.toFixed(0)} km
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="px-4 py-3 text-sm text-gray-600">Est. Food Waste</td>
|
||
<td className="px-4 py-3 text-sm text-right font-medium">
|
||
{(conventionalWaste * 0.25).toFixed(2)} kg
|
||
</td>
|
||
<td className="px-4 py-3 text-sm text-right text-gray-500">{conventionalWaste.toFixed(2)} kg</td>
|
||
<td className="px-4 py-3 text-sm text-right text-green-600 font-medium">
|
||
-{wasteSaved.toFixed(2)} kg
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Tangible equivalents */}
|
||
<div>
|
||
<button
|
||
onClick={() => setShowDetails(!showDetails)}
|
||
className="w-full flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition"
|
||
>
|
||
<span className="font-semibold text-gray-900">What does this mean?</span>
|
||
<span className="text-gray-500">{showDetails ? '▼' : '▶'}</span>
|
||
</button>
|
||
|
||
{showDetails && (
|
||
<div className="mt-4 grid grid-cols-2 md:grid-cols-3 gap-4">
|
||
<div className="p-4 bg-green-50 rounded-lg text-center">
|
||
<span className="text-3xl">🌲</span>
|
||
<p className="text-xl font-bold text-green-600 mt-2">
|
||
{equivalents.treesEquivalent.toFixed(1)}
|
||
</p>
|
||
<p className="text-xs text-gray-500">Trees planted (annual equivalent)</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-blue-50 rounded-lg text-center">
|
||
<span className="text-3xl">🚗</span>
|
||
<p className="text-xl font-bold text-blue-600 mt-2">
|
||
{equivalents.carMilesEquivalent.toFixed(0)}
|
||
</p>
|
||
<p className="text-xs text-gray-500">Car miles avoided</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-purple-50 rounded-lg text-center">
|
||
<span className="text-3xl">✈️</span>
|
||
<p className="text-xl font-bold text-purple-600 mt-2">
|
||
{equivalents.flightHoursEquivalent.toFixed(1)}
|
||
</p>
|
||
<p className="text-xs text-gray-500">Flight hours equivalent</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-cyan-50 rounded-lg text-center">
|
||
<span className="text-3xl">🚿</span>
|
||
<p className="text-xl font-bold text-cyan-600 mt-2">
|
||
{equivalents.showersEquivalent.toFixed(0)}
|
||
</p>
|
||
<p className="text-xs text-gray-500">Showers worth of water</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-orange-50 rounded-lg text-center">
|
||
<span className="text-3xl">⛽</span>
|
||
<p className="text-xl font-bold text-orange-600 mt-2">
|
||
{equivalents.gallonsGasoline.toFixed(1)}
|
||
</p>
|
||
<p className="text-xs text-gray-500">Gallons of gas saved</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-yellow-50 rounded-lg text-center">
|
||
<span className="text-3xl">🌍</span>
|
||
<p className="text-xl font-bold text-yellow-600 mt-2">
|
||
{carbonReduction.toFixed(0)}%
|
||
</p>
|
||
<p className="text-xs text-gray-500">Better for the planet</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Annual projection */}
|
||
<div className="p-4 bg-gradient-to-r from-emerald-50 to-teal-50 rounded-lg border border-emerald-200">
|
||
<h4 className="font-semibold text-emerald-800 mb-3">Annual Projection</h4>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<p className="text-2xl font-bold text-emerald-600">
|
||
{annualCarbonSaved.toFixed(0)} kg CO2
|
||
</p>
|
||
<p className="text-sm text-gray-600">Carbon saved per year</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-2xl font-bold text-teal-600">
|
||
{annualMilesSaved.toFixed(0)} km
|
||
</p>
|
||
<p className="text-sm text-gray-600">Food miles saved per year</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-xs text-gray-500 mt-3">
|
||
Based on current consumption of {produceWeightKg.toFixed(1)} kg produce
|
||
</p>
|
||
</div>
|
||
|
||
{/* Call to action */}
|
||
<div className="text-center">
|
||
<p className="text-sm text-gray-600">
|
||
Keep sourcing local produce to maximize your environmental impact!
|
||
</p>
|
||
<p className="text-xs text-gray-400 mt-1">
|
||
Every kilometer saved makes a difference
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|