localgreenchain/components/demand/RecommendationList.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

206 lines
7.6 KiB
TypeScript

import { PlantingRecommendation, RiskFactor } from '../../lib/demand/types';
interface RecommendationListProps {
recommendations: PlantingRecommendation[];
onSelect?: (recommendation: PlantingRecommendation) => void;
loading?: boolean;
}
const RISK_COLORS: Record<RiskFactor['severity'], string> = {
low: 'bg-green-100 text-green-800',
medium: 'bg-yellow-100 text-yellow-800',
high: 'bg-red-100 text-red-800',
};
const OVERALL_RISK_COLORS: Record<string, string> = {
low: 'text-green-600 bg-green-50 border-green-200',
medium: 'text-yellow-600 bg-yellow-50 border-yellow-200',
high: 'text-red-600 bg-red-50 border-red-200',
};
function formatCategory(category: string): string {
return category.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
export default function RecommendationList({
recommendations,
onSelect,
loading = false,
}: RecommendationListProps) {
if (loading) {
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Planting Recommendations</h3>
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="animate-pulse">
<div className="h-32 bg-gray-200 rounded-lg"></div>
</div>
))}
</div>
</div>
);
}
if (recommendations.length === 0) {
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Planting Recommendations</h3>
<div className="text-center py-8">
<div className="text-4xl mb-3">🌱</div>
<p className="text-gray-500">No recommendations available yet.</p>
<p className="text-sm text-gray-400 mt-1">
Check back later or update your preferences to get personalized recommendations.
</p>
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold text-gray-900">Planting Recommendations</h3>
<span className="text-sm text-gray-500">{recommendations.length} suggestions</span>
</div>
<div className="space-y-4">
{recommendations.map((rec) => (
<RecommendationCard key={rec.id} recommendation={rec} onSelect={onSelect} />
))}
</div>
</div>
);
}
function RecommendationCard({
recommendation: rec,
onSelect,
}: {
recommendation: PlantingRecommendation;
onSelect?: (recommendation: PlantingRecommendation) => void;
}) {
return (
<div
className={`border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition ${
onSelect ? 'cursor-pointer' : ''
}`}
onClick={() => onSelect?.(rec)}
>
{/* Header */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h4 className="text-lg font-semibold text-gray-900">{rec.produceType}</h4>
<p className="text-sm text-gray-500">
{rec.variety && <span className="mr-2">{rec.variety}</span>}
<span className="text-gray-400">|</span>
<span className="ml-2">{formatCategory(rec.category)}</span>
</p>
</div>
<div className={`px-3 py-1 rounded-full border ${OVERALL_RISK_COLORS[rec.overallRisk]}`}>
<span className="text-sm font-medium capitalize">{rec.overallRisk} Risk</span>
</div>
</div>
</div>
{/* Content */}
<div className="p-4 space-y-4">
{/* Key metrics */}
<div className="grid grid-cols-3 gap-3">
<div className="text-center p-2 bg-gray-50 rounded-lg">
<p className="text-lg font-bold text-gray-900">{rec.recommendedQuantity}</p>
<p className="text-xs text-gray-500">{rec.quantityUnit}</p>
</div>
<div className="text-center p-2 bg-blue-50 rounded-lg">
<p className="text-lg font-bold text-blue-600">{rec.expectedYieldKg.toFixed(1)} kg</p>
<p className="text-xs text-gray-500">Expected Yield</p>
</div>
<div className="text-center p-2 bg-green-50 rounded-lg">
<p className="text-lg font-bold text-green-600">${rec.projectedRevenue.toFixed(0)}</p>
<p className="text-xs text-gray-500">Est. Revenue</p>
</div>
</div>
{/* Timing */}
<div className="flex items-center justify-between text-sm p-3 bg-yellow-50 rounded-lg">
<div>
<span className="text-gray-600">Plant by:</span>
<span className="font-semibold text-gray-900 ml-2">{formatDate(rec.plantByDate)}</span>
</div>
<div className="text-gray-400">|</div>
<div>
<span className="text-gray-600">Harvest:</span>
<span className="font-semibold text-gray-900 ml-2">
{formatDate(rec.expectedHarvestStart)} - {formatDate(rec.expectedHarvestEnd)}
</span>
</div>
</div>
{/* Market opportunity */}
<div className="p-3 border border-gray-200 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">Market Opportunity</span>
<div className="flex items-center gap-1">
{[1, 2, 3, 4, 5].map((level) => (
<div
key={level}
className={`w-2 h-4 rounded-sm ${
level <= rec.marketConfidence / 20 ? 'bg-green-500' : 'bg-gray-200'
}`}
></div>
))}
<span className="text-xs text-gray-500 ml-1">{rec.marketConfidence}%</span>
</div>
</div>
<div className="text-sm">
<span className="text-gray-600">Projected demand:</span>
<span className="font-semibold text-gray-900 ml-1">{rec.projectedDemandKg.toFixed(0)} kg</span>
<span className="text-gray-400 mx-2">@</span>
<span className="font-semibold text-green-600">${rec.projectedPricePerKg.toFixed(2)}/kg</span>
</div>
</div>
{/* Risk factors */}
{rec.riskFactors.length > 0 && (
<div>
<p className="text-sm font-medium text-gray-700 mb-2">Risk Factors</p>
<div className="flex flex-wrap gap-2">
{rec.riskFactors.slice(0, 3).map((risk, index) => (
<span
key={index}
className={`text-xs px-2 py-1 rounded-full ${RISK_COLORS[risk.severity]}`}
title={risk.description}
>
{risk.type}
</span>
))}
{rec.riskFactors.length > 3 && (
<span className="text-xs px-2 py-1 rounded-full bg-gray-100 text-gray-600">
+{rec.riskFactors.length - 3} more
</span>
)}
</div>
</div>
)}
{/* Explanation */}
<p className="text-sm text-gray-600 italic">"{rec.explanation}"</p>
{/* Growing days indicator */}
<div className="flex items-center justify-between text-xs text-gray-500 pt-2 border-t border-gray-100">
<span>Growing period: {rec.growingDays} days</span>
<span>Yield confidence: {rec.yieldConfidence}%</span>
</div>
</div>
</div>
);
}