localgreenchain/components/analytics/SavingsCalculator.tsx
Claude 0cce5e2345
Add UI components for transport tracking, demand visualization, and analytics
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.
2025-11-22 18:34:51 +00:00

273 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}