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
117 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|