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

308 lines
11 KiB
TypeScript

import { useState } from 'react';
import { DemandItem, ProduceCategory } from '../../lib/demand/types';
interface SeasonalCalendarProps {
items: SeasonalItem[];
currentSeason?: 'spring' | 'summer' | 'fall' | 'winter';
}
export interface SeasonalItem {
produceType: string;
category: ProduceCategory;
seasonalAvailability: {
spring: boolean;
summer: boolean;
fall: boolean;
winter: boolean;
};
peakSeason?: 'spring' | 'summer' | 'fall' | 'winter';
}
const SEASONS = ['spring', 'summer', 'fall', 'winter'] as const;
const SEASON_COLORS: Record<string, { bg: string; border: string; text: string; icon: string }> = {
spring: { bg: 'bg-green-100', border: 'border-green-300', text: 'text-green-800', icon: '🌸' },
summer: { bg: 'bg-yellow-100', border: 'border-yellow-300', text: 'text-yellow-800', icon: '☀️' },
fall: { bg: 'bg-orange-100', border: 'border-orange-300', text: 'text-orange-800', icon: '🍂' },
winter: { bg: 'bg-blue-100', border: 'border-blue-300', text: 'text-blue-800', icon: '❄️' },
};
const CATEGORY_ICONS: Record<ProduceCategory, string> = {
leafy_greens: '🥬',
root_vegetables: '🥕',
nightshades: '🍅',
brassicas: '🥦',
alliums: '🧅',
legumes: '🫘',
squash: '🎃',
herbs: '🌿',
microgreens: '🌱',
sprouts: '🌾',
mushrooms: '🍄',
fruits: '🍎',
berries: '🍓',
citrus: '🍊',
tree_fruits: '🍑',
melons: '🍈',
edible_flowers: '🌸',
};
function formatCategory(category: string): string {
return category.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
}
function getCurrentSeason(): 'spring' | 'summer' | 'fall' | 'winter' {
const month = new Date().getMonth();
if (month >= 2 && month <= 4) return 'spring';
if (month >= 5 && month <= 7) return 'summer';
if (month >= 8 && month <= 10) return 'fall';
return 'winter';
}
export default function SeasonalCalendar({
items,
currentSeason = getCurrentSeason(),
}: SeasonalCalendarProps) {
const [selectedCategory, setSelectedCategory] = useState<ProduceCategory | 'all'>('all');
const [viewMode, setViewMode] = useState<'calendar' | 'list'>('calendar');
// Get unique categories
const categories = Array.from(new Set(items.map((item) => item.category)));
// Filter items by category
const filteredItems = selectedCategory === 'all'
? items
: items.filter((item) => item.category === selectedCategory);
// Group items by season for current view
const itemsBySeason: Record<string, SeasonalItem[]> = {
spring: [],
summer: [],
fall: [],
winter: [],
};
filteredItems.forEach((item) => {
SEASONS.forEach((season) => {
if (item.seasonalAvailability[season]) {
itemsBySeason[season].push(item);
}
});
});
// Get items available now
const availableNow = filteredItems.filter(
(item) => item.seasonalAvailability[currentSeason]
);
return (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-emerald-600 to-teal-600 text-white p-4">
<h3 className="text-xl font-bold">Seasonal Availability</h3>
<p className="text-emerald-200 text-sm">
Currently: {SEASON_COLORS[currentSeason].icon} {currentSeason.charAt(0).toUpperCase() + currentSeason.slice(1)}
</p>
</div>
{/* Controls */}
<div className="p-4 border-b border-gray-200 bg-gray-50">
<div className="flex flex-wrap items-center justify-between gap-4">
{/* Category filter */}
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600">Category:</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value as ProduceCategory | 'all')}
className="px-3 py-1 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500"
>
<option value="all">All Categories</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{CATEGORY_ICONS[cat]} {formatCategory(cat)}
</option>
))}
</select>
</div>
{/* View toggle */}
<div className="flex rounded-lg overflow-hidden border border-gray-300">
<button
onClick={() => setViewMode('calendar')}
className={`px-3 py-1 text-sm ${
viewMode === 'calendar'
? 'bg-green-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-100'
}`}
>
Calendar
</button>
<button
onClick={() => setViewMode('list')}
className={`px-3 py-1 text-sm ${
viewMode === 'list'
? 'bg-green-600 text-white'
: 'bg-white text-gray-600 hover:bg-gray-100'
}`}
>
List
</button>
</div>
</div>
</div>
<div className="p-6">
{/* Available Now Section */}
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
<span>{SEASON_COLORS[currentSeason].icon}</span>
Available Now ({availableNow.length} items)
</h4>
<div className="flex flex-wrap gap-2">
{availableNow.slice(0, 12).map((item, index) => (
<span
key={index}
className="px-3 py-1 bg-white border border-green-300 text-green-800 text-sm rounded-full"
>
{CATEGORY_ICONS[item.category]} {item.produceType}
</span>
))}
{availableNow.length > 12 && (
<span className="px-3 py-1 bg-green-200 text-green-800 text-sm rounded-full">
+{availableNow.length - 12} more
</span>
)}
</div>
</div>
{/* Calendar View */}
{viewMode === 'calendar' && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{SEASONS.map((season) => {
const seasonStyle = SEASON_COLORS[season];
const isCurrentSeason = season === currentSeason;
return (
<div
key={season}
className={`rounded-lg border-2 ${
isCurrentSeason ? seasonStyle.border : 'border-gray-200'
} overflow-hidden`}
>
<div className={`p-3 ${seasonStyle.bg} ${seasonStyle.text}`}>
<div className="flex items-center justify-between">
<span className="font-bold capitalize">
{seasonStyle.icon} {season}
</span>
<span className="text-sm">
{itemsBySeason[season].length} items
</span>
</div>
</div>
<div className="p-3 max-h-48 overflow-y-auto">
{itemsBySeason[season].length === 0 ? (
<p className="text-sm text-gray-400 text-center py-4">
No items in this season
</p>
) : (
<div className="space-y-1">
{itemsBySeason[season].map((item, index) => (
<div
key={index}
className={`text-sm p-1 rounded flex items-center gap-1 ${
item.peakSeason === season ? 'font-medium bg-gray-100' : ''
}`}
>
<span>{CATEGORY_ICONS[item.category]}</span>
<span>{item.produceType}</span>
{item.peakSeason === season && (
<span className="text-yellow-500 ml-auto" title="Peak season"></span>
)}
</div>
))}
</div>
)}
</div>
</div>
);
})}
</div>
)}
{/* List View */}
{viewMode === 'list' && (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 px-3 text-sm font-semibold text-gray-900">Item</th>
<th className="text-left py-2 px-3 text-sm font-semibold text-gray-900">Category</th>
{SEASONS.map((season) => (
<th
key={season}
className={`text-center py-2 px-3 text-sm font-semibold ${
season === currentSeason ? 'bg-green-50 text-green-800' : 'text-gray-900'
}`}
>
{SEASON_COLORS[season].icon}
<br />
<span className="text-xs capitalize">{season}</span>
</th>
))}
</tr>
</thead>
<tbody>
{filteredItems.map((item, index) => (
<tr
key={index}
className="border-b border-gray-100 hover:bg-gray-50"
>
<td className="py-2 px-3 text-sm">
<span className="mr-1">{CATEGORY_ICONS[item.category]}</span>
{item.produceType}
</td>
<td className="py-2 px-3 text-sm text-gray-500">
{formatCategory(item.category)}
</td>
{SEASONS.map((season) => (
<td
key={season}
className={`text-center py-2 px-3 ${
season === currentSeason ? 'bg-green-50' : ''
}`}
>
{item.seasonalAvailability[season] ? (
<span className={item.peakSeason === season ? 'text-yellow-500' : 'text-green-500'}>
{item.peakSeason === season ? '⭐' : '✓'}
</span>
) : (
<span className="text-gray-300">-</span>
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Legend */}
<div className="mt-6 pt-4 border-t border-gray-200 flex items-center justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<span className="text-green-500"></span>
<span className="text-gray-600">Available</span>
</div>
<div className="flex items-center gap-2">
<span className="text-yellow-500"></span>
<span className="text-gray-600">Peak Season</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-300">-</span>
<span className="text-gray-600">Not Available</span>
</div>
</div>
</div>
</div>
);
}