localgreenchain/components/vertical-farm/RecipeSelector.tsx
Claude 2f7f22ca22
Add vertical farming UI components and pages
Components:
- FarmCard: Farm summary display with status and metrics
- ZoneGrid: Multi-level zone layout visualization
- ZoneDetailCard: Individual zone details with environment readings
- EnvironmentGauge: Real-time environmental parameter display
- BatchProgress: Crop batch progress tracking with health scores
- RecipeSelector: Growing recipe browser and selector
- AlertPanel: Environment alerts display and management
- GrowthStageIndicator: Visual growth stage progress tracker
- ResourceUsageChart: Energy/water usage analytics visualization

Pages:
- /vertical-farm: Dashboard with farm listing and stats
- /vertical-farm/register: Multi-step farm registration form
- /vertical-farm/[farmId]: Farm detail view with zones and alerts
- /vertical-farm/[farmId]/zones: Zone management with batch starting
- /vertical-farm/[farmId]/batches: Batch management and harvesting
- /vertical-farm/[farmId]/analytics: Farm analytics and performance metrics
2025-11-22 18:35:57 +00:00

117 lines
3.7 KiB
TypeScript

import { useState, useEffect } from 'react';
import { GrowingRecipe } from '../../lib/vertical-farming/types';
interface RecipeSelectorProps {
onSelect: (recipe: GrowingRecipe) => void;
selectedRecipeId?: string;
}
export default function RecipeSelector({ onSelect, selectedRecipeId }: RecipeSelectorProps) {
const [recipes, setRecipes] = useState<GrowingRecipe[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
useEffect(() => {
fetchRecipes();
}, []);
const fetchRecipes = async () => {
try {
const response = await fetch('/api/vertical-farm/recipes');
const data = await response.json();
if (data.success) {
setRecipes(data.recipes);
}
} catch (error) {
console.error('Error fetching recipes:', error);
} finally {
setLoading(false);
}
};
const filteredRecipes = recipes.filter(
r =>
r.name.toLowerCase().includes(filter.toLowerCase()) ||
r.cropType.toLowerCase().includes(filter.toLowerCase())
);
if (loading) {
return (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600 mx-auto" />
<p className="text-gray-600 mt-2">Loading recipes...</p>
</div>
);
}
return (
<div className="space-y-4">
<input
type="text"
placeholder="Search recipes..."
value={filter}
onChange={e => setFilter(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto">
{filteredRecipes.map(recipe => (
<div
key={recipe.id}
onClick={() => onSelect(recipe)}
className={`p-4 rounded-lg border-2 cursor-pointer transition ${
selectedRecipeId === recipe.id
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-green-300'
}`}
>
<div className="flex justify-between items-start mb-2">
<h4 className="font-semibold text-gray-900">{recipe.name}</h4>
{recipe.rating && (
<span className="text-sm text-yellow-600">
{'*'.repeat(Math.round(recipe.rating))} {recipe.rating.toFixed(1)}
</span>
)}
</div>
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600 mb-2">
<div>
<span className="font-medium">Crop:</span> {recipe.cropType}
</div>
<div>
<span className="font-medium">Days:</span> {recipe.expectedDays}
</div>
<div>
<span className="font-medium">Yield:</span> {recipe.expectedYieldGrams}g
</div>
<div>
<span className="font-medium">Uses:</span> {recipe.timesUsed}
</div>
</div>
<div className="flex flex-wrap gap-1">
{recipe.stages.map(stage => (
<span
key={stage.name}
className="px-2 py-0.5 bg-gray-100 rounded text-xs text-gray-700"
>
{stage.name}
</span>
))}
</div>
<div className="mt-2 text-xs text-gray-500">
Source: {recipe.source} | Zone: {recipe.requirements.zoneType}
</div>
</div>
))}
</div>
{filteredRecipes.length === 0 && (
<p className="text-center text-gray-600 py-4">
No recipes found matching "{filter}"
</p>
)}
</div>
);
}