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.
272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
import { EnvironmentalImpact as ImpactData, TransportEventType, TransportMethod } from '../../lib/transport/types';
|
|
|
|
interface EnvironmentalImpactProps {
|
|
impact: ImpactData;
|
|
title?: string;
|
|
showDetails?: boolean;
|
|
}
|
|
|
|
const METHOD_LABELS: Record<TransportMethod, string> = {
|
|
walking: 'Walking',
|
|
bicycle: 'Bicycle',
|
|
electric_vehicle: 'Electric Vehicle',
|
|
hybrid_vehicle: 'Hybrid Vehicle',
|
|
gasoline_vehicle: 'Gas Vehicle',
|
|
diesel_truck: 'Diesel Truck',
|
|
electric_truck: 'Electric Truck',
|
|
refrigerated_truck: 'Refrigerated Truck',
|
|
rail: 'Rail',
|
|
ship: 'Ship',
|
|
air: 'Air Freight',
|
|
drone: 'Drone',
|
|
local_delivery: 'Local Delivery',
|
|
customer_pickup: 'Customer Pickup',
|
|
};
|
|
|
|
const EVENT_LABELS: Record<TransportEventType, string> = {
|
|
seed_acquisition: 'Seed Acquisition',
|
|
planting: 'Planting',
|
|
growing_transport: 'Growing Transport',
|
|
harvest: 'Harvest',
|
|
processing: 'Processing',
|
|
distribution: 'Distribution',
|
|
consumer_delivery: 'Consumer Delivery',
|
|
seed_saving: 'Seed Saving',
|
|
seed_sharing: 'Seed Sharing',
|
|
};
|
|
|
|
function getCarbonRating(carbon: number): { label: string; color: string; bgColor: string } {
|
|
if (carbon < 0.5) return { label: 'Excellent', color: 'text-green-600', bgColor: 'bg-green-100' };
|
|
if (carbon < 1) return { label: 'Very Good', color: 'text-emerald-600', bgColor: 'bg-emerald-100' };
|
|
if (carbon < 2) return { label: 'Good', color: 'text-lime-600', bgColor: 'bg-lime-100' };
|
|
if (carbon < 5) return { label: 'Moderate', color: 'text-yellow-600', bgColor: 'bg-yellow-100' };
|
|
if (carbon < 10) return { label: 'High', color: 'text-orange-600', bgColor: 'bg-orange-100' };
|
|
return { label: 'Very High', color: 'text-red-600', bgColor: 'bg-red-100' };
|
|
}
|
|
|
|
export default function EnvironmentalImpact({
|
|
impact,
|
|
title = 'Environmental Impact Report',
|
|
showDetails = true,
|
|
}: EnvironmentalImpactProps) {
|
|
const rating = getCarbonRating(impact.totalCarbonKg);
|
|
|
|
// Sort methods by carbon impact
|
|
const sortedMethods = Object.entries(impact.breakdownByMethod)
|
|
.filter(([_, data]) => data.carbon > 0 || data.distance > 0)
|
|
.sort((a, b) => b[1].carbon - a[1].carbon);
|
|
|
|
// Sort events by count
|
|
const sortedEvents = Object.entries(impact.breakdownByEventType)
|
|
.filter(([_, data]) => data.count > 0)
|
|
.sort((a, b) => b[1].count - a[1].count);
|
|
|
|
const totalMethods = sortedMethods.length;
|
|
const totalEvents = sortedEvents.reduce((sum, [_, data]) => sum + data.count, 0);
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-teal-600 to-cyan-600 text-white p-6">
|
|
<h3 className="text-xl font-bold">{title}</h3>
|
|
<p className="text-teal-200 text-sm mt-1">Complete carbon and food miles analysis</p>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-6">
|
|
{/* Main metrics */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="text-center p-4 bg-green-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-green-600">{impact.totalCarbonKg.toFixed(2)}</p>
|
|
<p className="text-sm text-gray-500">Total Carbon (kg CO2)</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-blue-600">{impact.totalFoodMiles.toFixed(1)}</p>
|
|
<p className="text-sm text-gray-500">Total Food Miles (km)</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-purple-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-purple-600">{impact.carbonPerKgProduce.toFixed(3)}</p>
|
|
<p className="text-sm text-gray-500">kg CO2 / kg Produce</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-orange-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-orange-600">{impact.milesPerKgProduce.toFixed(1)}</p>
|
|
<p className="text-sm text-gray-500">Miles / kg Produce</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rating badge */}
|
|
<div className="flex justify-center">
|
|
<div className={`px-6 py-3 rounded-full ${rating.bgColor}`}>
|
|
<span className={`text-lg font-bold ${rating.color}`}>
|
|
Environmental Rating: {rating.label}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Comparison with conventional */}
|
|
{impact.comparisonToConventional && (
|
|
<div className="p-6 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg border border-green-200">
|
|
<h4 className="text-lg font-bold text-gray-900 mb-4">
|
|
Comparison vs Conventional Agriculture
|
|
</h4>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="text-center">
|
|
<div className="w-24 h-24 mx-auto mb-3 rounded-full bg-green-100 flex items-center justify-center">
|
|
<span className="text-3xl font-bold text-green-600">
|
|
{impact.comparisonToConventional.percentageReduction.toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600">Carbon Reduction</p>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<p className="text-3xl font-bold text-green-600">
|
|
{impact.comparisonToConventional.carbonSaved.toFixed(1)}
|
|
</p>
|
|
<p className="text-sm text-gray-500">kg CO2 Saved</p>
|
|
<div className="mt-2 flex items-center justify-center gap-1 text-green-600">
|
|
<span>🌲</span>
|
|
<span className="text-xs">
|
|
= {(impact.comparisonToConventional.carbonSaved / 21).toFixed(1)} trees/year
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<p className="text-3xl font-bold text-blue-600">
|
|
{impact.comparisonToConventional.milesSaved.toFixed(0)}
|
|
</p>
|
|
<p className="text-sm text-gray-500">km Saved</p>
|
|
<div className="mt-2 flex items-center justify-center gap-1 text-blue-600">
|
|
<span>🚗</span>
|
|
<span className="text-xs">
|
|
= {(impact.comparisonToConventional.milesSaved / 15).toFixed(0)} car trips
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Visual bar comparison */}
|
|
<div className="mt-6 space-y-3">
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-gray-600">Your Carbon Footprint</span>
|
|
<span className="font-medium text-green-600">{impact.totalCarbonKg.toFixed(2)} kg</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
|
|
<div
|
|
className="h-full bg-green-500 rounded-full"
|
|
style={{
|
|
width: `${100 - impact.comparisonToConventional.percentageReduction}%`
|
|
}}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-gray-600">Conventional Average</span>
|
|
<span className="font-medium text-gray-500">
|
|
{(impact.totalCarbonKg + impact.comparisonToConventional.carbonSaved).toFixed(2)} kg
|
|
</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
|
|
<div className="h-full bg-gray-400 rounded-full w-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Detailed breakdowns */}
|
|
{showDetails && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* By Transport Method */}
|
|
{sortedMethods.length > 0 && (
|
|
<div className="p-4 border border-gray-200 rounded-lg">
|
|
<h4 className="font-semibold text-gray-900 mb-4">By Transport Method</h4>
|
|
<div className="space-y-3">
|
|
{sortedMethods.map(([method, data]) => {
|
|
const percentage = impact.totalCarbonKg > 0
|
|
? (data.carbon / impact.totalCarbonKg) * 100
|
|
: 0;
|
|
return (
|
|
<div key={method}>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-gray-600">
|
|
{METHOD_LABELS[method as TransportMethod] || method}
|
|
</span>
|
|
<span className="font-medium text-gray-900">
|
|
{data.carbon.toFixed(3)} kg ({percentage.toFixed(0)}%)
|
|
</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="h-full bg-teal-500 rounded-full"
|
|
style={{ width: `${percentage}%` }}
|
|
></div>
|
|
</div>
|
|
<p className="text-xs text-gray-400 mt-0.5">
|
|
{data.distance.toFixed(1)} km traveled
|
|
</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* By Event Type */}
|
|
{sortedEvents.length > 0 && (
|
|
<div className="p-4 border border-gray-200 rounded-lg">
|
|
<h4 className="font-semibold text-gray-900 mb-4">By Event Type</h4>
|
|
<div className="space-y-3">
|
|
{sortedEvents.map(([eventType, data]) => {
|
|
const percentage = totalEvents > 0
|
|
? (data.count / totalEvents) * 100
|
|
: 0;
|
|
return (
|
|
<div key={eventType}>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-gray-600">
|
|
{EVENT_LABELS[eventType as TransportEventType] || eventType}
|
|
</span>
|
|
<span className="font-medium text-gray-900">
|
|
{data.count} events • {data.carbon.toFixed(3)} kg
|
|
</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="h-full bg-cyan-500 rounded-full"
|
|
style={{ width: `${percentage}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Tips Section */}
|
|
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
<h4 className="font-semibold text-yellow-800 mb-2">Tips to Reduce Impact</h4>
|
|
<ul className="text-sm text-yellow-700 space-y-1">
|
|
<li>• Prefer walking, cycling, or electric vehicles for short distances</li>
|
|
<li>• Consolidate multiple transports into single trips</li>
|
|
<li>• Source from local producers within 25km when possible</li>
|
|
<li>• Use rail transport for longer distances when available</li>
|
|
<li>• Avoid air freight unless absolutely necessary</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Summary stats */}
|
|
<div className="flex justify-between text-xs text-gray-500 pt-4 border-t border-gray-200">
|
|
<span>Transport methods used: {totalMethods}</span>
|
|
<span>Total events tracked: {totalEvents}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|