localgreenchain/pages/vertical-farm/register.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

492 lines
20 KiB
TypeScript

import { useState } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Head from 'next/head';
import { VerticalFarm, FacilitySpecs, GrowingZone } from '../../lib/vertical-farming/types';
export default function RegisterVerticalFarm() {
const router = useRouter();
const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
name: '',
address: '',
city: '',
country: '',
latitude: '',
longitude: '',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
totalAreaSqm: '',
growingAreaSqm: '',
numberOfLevels: '1',
ceilingHeightM: '3',
totalGrowingPositions: '',
powerCapacityKw: '',
waterStorageL: '',
buildingType: 'warehouse' as const,
automationLevel: 'semi_automated' as const,
zones: [] as Partial<GrowingZone>[],
});
const updateField = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const addZone = () => {
const newZone: Partial<GrowingZone> = {
id: `zone-${Date.now()}`,
name: `Zone ${formData.zones.length + 1}`,
level: 1,
areaSqm: 10,
lengthM: 5,
widthM: 2,
growingMethod: 'NFT',
plantPositions: 100,
status: 'empty',
plantIds: [],
};
setFormData(prev => ({ ...prev, zones: [...prev.zones, newZone] }));
};
const updateZone = (index: number, updates: Partial<GrowingZone>) => {
setFormData(prev => ({
...prev,
zones: prev.zones.map((z, i) => (i === index ? { ...z, ...updates } : z)),
}));
};
const removeZone = (index: number) => {
setFormData(prev => ({
...prev,
zones: prev.zones.filter((_, i) => i !== index),
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const farmData = {
name: formData.name,
location: {
latitude: parseFloat(formData.latitude),
longitude: parseFloat(formData.longitude),
address: formData.address,
city: formData.city,
country: formData.country,
timezone: formData.timezone,
},
specs: {
totalAreaSqm: parseFloat(formData.totalAreaSqm),
growingAreaSqm: parseFloat(formData.growingAreaSqm),
numberOfLevels: parseInt(formData.numberOfLevels),
ceilingHeightM: parseFloat(formData.ceilingHeightM),
totalGrowingPositions: parseInt(formData.totalGrowingPositions),
currentActivePlants: 0,
powerCapacityKw: parseFloat(formData.powerCapacityKw) || 0,
waterStorageL: parseFloat(formData.waterStorageL) || 0,
backupPowerHours: 0,
certifications: [],
buildingType: formData.buildingType,
insulation: 'standard',
},
zones: formData.zones,
automationLevel: formData.automationLevel,
};
const response = await fetch('/api/vertical-farm/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(farmData),
});
const data = await response.json();
if (data.success) {
router.push(`/vertical-farm/${data.farm.id}`);
} else {
setError(data.error || 'Failed to register farm');
}
} catch (err) {
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<Head>
<title>Register Vertical Farm - LocalGreenChain</title>
</Head>
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<Link href="/">
<a className="text-2xl font-bold text-green-800">LocalGreenChain</a>
</Link>
<Link href="/vertical-farm">
<a className="text-gray-600 hover:text-gray-900">Back to Dashboard</a>
</Link>
</div>
</div>
</header>
<main className="max-w-3xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-gray-900 mb-8">Register Vertical Farm</h1>
{/* Progress Steps */}
<div className="flex items-center justify-center mb-8">
{[1, 2, 3].map(s => (
<div key={s} className="flex items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold ${
step >= s ? 'bg-green-600 text-white' : 'bg-gray-200 text-gray-600'
}`}
>
{s}
</div>
{s < 3 && (
<div className={`w-20 h-1 ${step > s ? 'bg-green-600' : 'bg-gray-200'}`} />
)}
</div>
))}
</div>
<div className="flex justify-center gap-8 text-sm text-gray-600 mb-8">
<span className={step === 1 ? 'font-semibold text-green-600' : ''}>Basic Info</span>
<span className={step === 2 ? 'font-semibold text-green-600' : ''}>Facility Specs</span>
<span className={step === 3 ? 'font-semibold text-green-600' : ''}>Zones</span>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-lg p-6">
{/* Step 1: Basic Info */}
{step === 1 && (
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Basic Information</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Farm Name *</label>
<input
type="text"
required
value={formData.name}
onChange={e => updateField('name', e.target.value)}
placeholder="My Vertical Farm"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Address *</label>
<input
type="text"
required
value={formData.address}
onChange={e => updateField('address', e.target.value)}
placeholder="123 Farm Street"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
<input
type="text"
required
value={formData.city}
onChange={e => updateField('city', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Country *</label>
<input
type="text"
required
value={formData.country}
onChange={e => updateField('country', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Latitude</label>
<input
type="number"
step="any"
value={formData.latitude}
onChange={e => updateField('latitude', e.target.value)}
placeholder="40.7128"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Longitude</label>
<input
type="number"
step="any"
value={formData.longitude}
onChange={e => updateField('longitude', e.target.value)}
placeholder="-74.0060"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
</div>
)}
{/* Step 2: Facility Specs */}
{step === 2 && (
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Facility Specifications</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Total Area (m²) *</label>
<input
type="number"
required
value={formData.totalAreaSqm}
onChange={e => updateField('totalAreaSqm', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Growing Area (m²) *</label>
<input
type="number"
required
value={formData.growingAreaSqm}
onChange={e => updateField('growingAreaSqm', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Number of Levels *</label>
<input
type="number"
required
min="1"
value={formData.numberOfLevels}
onChange={e => updateField('numberOfLevels', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Ceiling Height (m)</label>
<input
type="number"
step="0.1"
value={formData.ceilingHeightM}
onChange={e => updateField('ceilingHeightM', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Total Growing Positions *</label>
<input
type="number"
required
value={formData.totalGrowingPositions}
onChange={e => updateField('totalGrowingPositions', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Power Capacity (kW)</label>
<input
type="number"
value={formData.powerCapacityKw}
onChange={e => updateField('powerCapacityKw', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Water Storage (L)</label>
<input
type="number"
value={formData.waterStorageL}
onChange={e => updateField('waterStorageL', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Building Type</label>
<select
value={formData.buildingType}
onChange={e => updateField('buildingType', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="warehouse">Warehouse</option>
<option value="greenhouse">Greenhouse</option>
<option value="container">Container</option>
<option value="purpose_built">Purpose Built</option>
<option value="retrofit">Retrofit</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Automation Level</label>
<select
value={formData.automationLevel}
onChange={e => updateField('automationLevel', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="manual">Manual</option>
<option value="semi_automated">Semi-Automated</option>
<option value="fully_automated">Fully Automated</option>
</select>
</div>
</div>
</div>
)}
{/* Step 3: Zones */}
{step === 3 && (
<div className="space-y-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900">Growing Zones</h2>
<button
type="button"
onClick={addZone}
className="px-4 py-2 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition"
>
+ Add Zone
</button>
</div>
{formData.zones.length === 0 ? (
<div className="text-center py-8 bg-gray-50 rounded-lg">
<p className="text-gray-600">No zones added yet. Click "Add Zone" to create your first growing zone.</p>
</div>
) : (
<div className="space-y-4">
{formData.zones.map((zone, index) => (
<div key={zone.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex justify-between items-start mb-3">
<input
type="text"
value={zone.name}
onChange={e => updateZone(index, { name: e.target.value })}
className="text-lg font-medium border-none focus:ring-0 p-0"
/>
<button
type="button"
onClick={() => removeZone(index)}
className="text-red-600 hover:text-red-800"
>
Remove
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div>
<label className="block text-xs text-gray-600 mb-1">Level</label>
<input
type="number"
min="1"
value={zone.level}
onChange={e => updateZone(index, { level: parseInt(e.target.value) })}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Area (m²)</label>
<input
type="number"
value={zone.areaSqm}
onChange={e => updateZone(index, { areaSqm: parseFloat(e.target.value) })}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Positions</label>
<input
type="number"
value={zone.plantPositions}
onChange={e => updateZone(index, { plantPositions: parseInt(e.target.value) })}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Method</label>
<select
value={zone.growingMethod}
onChange={e => updateZone(index, { growingMethod: e.target.value as any })}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
>
<option value="NFT">NFT</option>
<option value="DWC">DWC</option>
<option value="ebb_flow">Ebb & Flow</option>
<option value="aeroponics">Aeroponics</option>
<option value="vertical_towers">Vertical Towers</option>
<option value="rack_system">Rack System</option>
</select>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Navigation Buttons */}
<div className="flex justify-between mt-8 pt-6 border-t border-gray-200">
{step > 1 ? (
<button
type="button"
onClick={() => setStep(step - 1)}
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
>
Previous
</button>
) : (
<div />
)}
{step < 3 ? (
<button
type="button"
onClick={() => setStep(step + 1)}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
>
Next
</button>
) : (
<button
type="submit"
disabled={loading}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition disabled:opacity-50"
>
{loading ? 'Registering...' : 'Register Farm'}
</button>
)}
</div>
</form>
</main>
</div>
);
}