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.
231 lines
9 KiB
TypeScript
231 lines
9 KiB
TypeScript
import { useState } from 'react';
|
|
|
|
interface FoodMilesTrackerProps {
|
|
data: FoodMilesData[];
|
|
currentTotal: number;
|
|
targetMiles?: number;
|
|
}
|
|
|
|
export interface FoodMilesData {
|
|
date: string;
|
|
miles: number;
|
|
carbonKg: number;
|
|
eventType?: string;
|
|
produceType?: string;
|
|
}
|
|
|
|
type TimeRange = '7d' | '30d' | '90d' | '365d';
|
|
|
|
export default function FoodMilesTracker({
|
|
data,
|
|
currentTotal,
|
|
targetMiles = 50,
|
|
}: FoodMilesTrackerProps) {
|
|
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
|
|
|
// Filter data based on time range
|
|
const filterData = (range: TimeRange): FoodMilesData[] => {
|
|
const now = new Date();
|
|
const daysMap: Record<TimeRange, number> = {
|
|
'7d': 7,
|
|
'30d': 30,
|
|
'90d': 90,
|
|
'365d': 365,
|
|
};
|
|
const cutoff = new Date(now.getTime() - daysMap[range] * 24 * 60 * 60 * 1000);
|
|
return data.filter((d) => new Date(d.date) >= cutoff);
|
|
};
|
|
|
|
const filteredData = filterData(timeRange);
|
|
|
|
// Calculate stats for filtered period
|
|
const totalMiles = filteredData.reduce((sum, d) => sum + d.miles, 0);
|
|
const totalCarbon = filteredData.reduce((sum, d) => sum + d.carbonKg, 0);
|
|
const avgMilesPerDay = filteredData.length > 0 ? totalMiles / filteredData.length : 0;
|
|
|
|
// Group data by day for chart
|
|
const dailyData: Record<string, { miles: number; carbon: number }> = {};
|
|
filteredData.forEach((d) => {
|
|
const day = d.date.split('T')[0];
|
|
if (!dailyData[day]) {
|
|
dailyData[day] = { miles: 0, carbon: 0 };
|
|
}
|
|
dailyData[day].miles += d.miles;
|
|
dailyData[day].carbon += d.carbonKg;
|
|
});
|
|
|
|
const chartData = Object.entries(dailyData).sort((a, b) => a[0].localeCompare(b[0]));
|
|
const maxMiles = Math.max(...chartData.map(([_, d]) => d.miles), 1);
|
|
|
|
// Progress towards target
|
|
const progressPercentage = Math.min((currentTotal / targetMiles) * 100, 100);
|
|
const isOverTarget = currentTotal > targetMiles;
|
|
|
|
// Recent events
|
|
const recentEvents = [...data].sort(
|
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
|
).slice(0, 5);
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white p-6">
|
|
<h3 className="text-xl font-bold">Food Miles Tracker</h3>
|
|
<p className="text-blue-200 text-sm mt-1">Monitor your produce transportation distances</p>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-6">
|
|
{/* Time range selector */}
|
|
<div className="flex justify-center">
|
|
<div className="inline-flex rounded-lg overflow-hidden border border-gray-300">
|
|
{(['7d', '30d', '90d', '365d'] as TimeRange[]).map((range) => (
|
|
<button
|
|
key={range}
|
|
onClick={() => setTimeRange(range)}
|
|
className={`px-4 py-2 text-sm font-medium ${
|
|
timeRange === range
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-white text-gray-600 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : range === '90d' ? '90 Days' : '1 Year'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Current total with target */}
|
|
<div className="p-6 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Current Period Total</p>
|
|
<p className={`text-4xl font-bold ${isOverTarget ? 'text-orange-600' : 'text-blue-600'}`}>
|
|
{totalMiles.toFixed(1)} km
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm text-gray-600">Target</p>
|
|
<p className="text-2xl font-bold text-gray-400">{targetMiles} km</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress bar */}
|
|
<div className="relative">
|
|
<div className="w-full bg-gray-200 rounded-full h-4">
|
|
<div
|
|
className={`h-4 rounded-full transition-all ${
|
|
isOverTarget ? 'bg-orange-500' : 'bg-blue-500'
|
|
}`}
|
|
style={{ width: `${progressPercentage}%` }}
|
|
></div>
|
|
</div>
|
|
{/* Target marker */}
|
|
<div
|
|
className="absolute top-0 w-0.5 h-6 bg-gray-800 -mt-1"
|
|
style={{ left: '100%' }}
|
|
>
|
|
<div className="absolute -top-5 -left-4 text-xs text-gray-500">Target</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className={`text-sm mt-2 ${isOverTarget ? 'text-orange-600' : 'text-green-600'}`}>
|
|
{isOverTarget
|
|
? `${(currentTotal - targetMiles).toFixed(1)} km over target`
|
|
: `${(targetMiles - currentTotal).toFixed(1)} km remaining to target`}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats row */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
|
<p className="text-2xl font-bold text-gray-900">{filteredData.length}</p>
|
|
<p className="text-xs text-gray-500">Transport Events</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-green-50 rounded-lg">
|
|
<p className="text-2xl font-bold text-green-600">{totalCarbon.toFixed(2)}</p>
|
|
<p className="text-xs text-gray-500">kg CO2 Total</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
|
<p className="text-2xl font-bold text-blue-600">{avgMilesPerDay.toFixed(1)}</p>
|
|
<p className="text-xs text-gray-500">Avg km/day</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Simple bar chart */}
|
|
{chartData.length > 0 && (
|
|
<div className="p-4 bg-gray-50 rounded-lg">
|
|
<h4 className="text-sm font-semibold text-gray-900 mb-4">Daily Food Miles</h4>
|
|
<div className="flex items-end gap-1 h-32">
|
|
{chartData.slice(-14).map(([date, values], index) => {
|
|
const height = (values.miles / maxMiles) * 100;
|
|
return (
|
|
<div
|
|
key={date}
|
|
className="flex-1 flex flex-col items-center group"
|
|
>
|
|
<div className="w-full relative flex-1 flex items-end">
|
|
<div
|
|
className="w-full bg-blue-500 rounded-t hover:bg-blue-600 transition cursor-pointer"
|
|
style={{ height: `${Math.max(height, 2)}%` }}
|
|
title={`${date}: ${values.miles.toFixed(1)} km`}
|
|
></div>
|
|
</div>
|
|
<div className="text-xs text-gray-400 mt-1 rotate-45 origin-top-left">
|
|
{new Date(date).getDate()}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="text-xs text-gray-400 text-center mt-2">Last 14 days</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent events */}
|
|
{recentEvents.length > 0 && (
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-gray-900 mb-3">Recent Transport Events</h4>
|
|
<div className="space-y-2">
|
|
{recentEvents.map((event, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-gray-900">
|
|
{event.produceType || event.eventType || 'Transport'}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{new Date(event.date).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-bold text-blue-600">{event.miles.toFixed(1)} km</p>
|
|
<p className="text-xs text-gray-500">{event.carbonKg.toFixed(3)} kg CO2</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Comparison info */}
|
|
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
<h4 className="font-semibold text-green-800 mb-2">Did you know?</h4>
|
|
<p className="text-sm text-green-700">
|
|
The average food item in conventional supply chains travels <strong>2,400 km</strong> before reaching your plate.
|
|
LocalGreenChain helps reduce this by connecting you with local growers.
|
|
</p>
|
|
<div className="mt-3 text-sm text-green-600">
|
|
Your average: <strong>{avgMilesPerDay.toFixed(1)} km/day</strong> vs Conventional: <strong>6.6 km/day</strong>
|
|
{avgMilesPerDay < 6.6 && (
|
|
<span className="ml-2 text-green-500">
|
|
({((1 - avgMilesPerDay / 6.6) * 100).toFixed(0)}% better!)
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|