localgreenchain/components/analytics/FoodMilesTracker.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

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