Add environmental tracking UI components and integrations
Implemented comprehensive UI for displaying and managing environmental data, with smart recommendations and health scoring. Components Created: - EnvironmentalForm.tsx - Multi-section form for all environmental data * Tabbed interface for 8 environmental categories * Soil composition (type, pH, texture, drainage, amendments) * Nutrients (NPK, micronutrients, EC/TDS) * Lighting (natural/artificial with detailed metrics) * Climate (temperature, humidity, airflow, zones) * Location (indoor/outdoor, growing type) * Container (type, size, drainage warnings) * Watering (method, source, quality) * Surroundings (ecosystem, wind, companions) * Real-time validation and helpful tips - EnvironmentalDisplay.tsx - Beautiful display of environmental data * Environmental health score with color coding (0-100) * Priority-based recommendations (critical → low) * Organized sections for soil, climate, lighting, etc. * Data points with smart formatting * Warning highlights for critical issues * Integration with recommendations API Plant Detail Page Integration: - Added Environment tab to plant detail page - Shows full environmental data when available - Displays recommendations with health score - Empty state with educational content when no data - Link to Environmental Tracking Guide - Call-to-action to add environmental data Features: - Health Score: 0-100 rating with color-coded progress bar - Smart Recommendations: Auto-fetched, priority-sorted advice - Critical Warnings: Red highlights for no drainage, extreme values - Helpful Tips: Inline guidance for each section - Responsive Design: Works on mobile and desktop - Real-time Validation: pH ranges, temperature warnings Environmental Health Scoring: - 90-100: Excellent conditions - 75-89: Good conditions - 60-74: Adequate conditions - 40-59: Suboptimal conditions - 0-39: Poor conditions Recommendation Priorities: - 🚨 Critical: No drainage, extreme conditions - ⚠️ High: pH problems, insufficient light - 💡 Medium: Humidity, organic matter - ℹ️ Low: Water quality tweaks User Experience Improvements: - "Add" badge on Environment tab when no data - Educational empty states explaining benefits - Smart formatting (snake_case → Title Case) - Color-coded health indicators - Expandable sections to prevent overwhelming UI - Context-aware tips and recommendations This enables users to: - Easily input environmental data - See personalized recommendations - Monitor environmental health - Learn best practices inline - Track improvements over time
This commit is contained in:
parent
372fd0a015
commit
8b06ccb7d0
3 changed files with 1124 additions and 1 deletions
334
components/EnvironmentalDisplay.tsx
Normal file
334
components/EnvironmentalDisplay.tsx
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { GrowingEnvironment } from '../lib/environment/types';
|
||||||
|
import { EnvironmentalRecommendation } from '../lib/environment/types';
|
||||||
|
|
||||||
|
interface EnvironmentalDisplayProps {
|
||||||
|
environment: GrowingEnvironment;
|
||||||
|
plantId: string;
|
||||||
|
showRecommendations?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EnvironmentalDisplay({
|
||||||
|
environment,
|
||||||
|
plantId,
|
||||||
|
showRecommendations = true,
|
||||||
|
}: EnvironmentalDisplayProps) {
|
||||||
|
const [recommendations, setRecommendations] = useState<EnvironmentalRecommendation[]>([]);
|
||||||
|
const [healthScore, setHealthScore] = useState<number | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showRecommendations) {
|
||||||
|
fetchRecommendations();
|
||||||
|
}
|
||||||
|
}, [plantId]);
|
||||||
|
|
||||||
|
const fetchRecommendations = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/environment/recommendations?plantId=${plantId}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setRecommendations(data.recommendations);
|
||||||
|
setHealthScore(data.environmentalHealth);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching recommendations:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Environmental Health Score */}
|
||||||
|
{healthScore !== null && (
|
||||||
|
<div className="bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg p-6 border border-green-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
||||||
|
Environmental Health Score
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Overall assessment of growing conditions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className={`text-5xl font-bold ${getScoreColor(healthScore)}`}>
|
||||||
|
{healthScore}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 mt-1">/ 100</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||||
|
<div
|
||||||
|
className={`h-3 rounded-full transition-all ${getScoreBgColor(healthScore)}`}
|
||||||
|
style={{ width: `${healthScore}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 mt-2">{getScoreInterpretation(healthScore)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recommendations */}
|
||||||
|
{showRecommendations && recommendations.length > 0 && (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">
|
||||||
|
📋 Recommendations ({recommendations.length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{recommendations.map((rec, idx) => (
|
||||||
|
<RecommendationCard key={idx} recommendation={rec} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Soil Information */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||||
|
🌱 Soil Composition
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
<DataPoint label="Type" value={formatValue(environment.soil.type)} />
|
||||||
|
<DataPoint label="pH" value={environment.soil.pH.toFixed(1)} />
|
||||||
|
<DataPoint label="Texture" value={formatValue(environment.soil.texture)} />
|
||||||
|
<DataPoint label="Drainage" value={formatValue(environment.soil.drainage)} />
|
||||||
|
<DataPoint label="Organic Matter" value={`${environment.soil.organicMatter}%`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Climate Conditions */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||||
|
🌡️ Climate Conditions
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
<DataPoint label="Day Temp" value={`${environment.climate.temperatureDay}°C`} />
|
||||||
|
<DataPoint label="Night Temp" value={`${environment.climate.temperatureNight}°C`} />
|
||||||
|
<DataPoint label="Humidity" value={`${environment.climate.humidityAverage}%`} />
|
||||||
|
<DataPoint label="Airflow" value={formatValue(environment.climate.airflow)} />
|
||||||
|
<DataPoint label="Ventilation" value={formatValue(environment.climate.ventilation)} />
|
||||||
|
{environment.climate.zone && (
|
||||||
|
<DataPoint label="Hardiness Zone" value={environment.climate.zone} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lighting */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||||
|
☀️ Lighting
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
<DataPoint label="Type" value={formatValue(environment.lighting.type)} />
|
||||||
|
{environment.lighting.naturalLight && (
|
||||||
|
<>
|
||||||
|
<DataPoint
|
||||||
|
label="Exposure"
|
||||||
|
value={formatValue(environment.lighting.naturalLight.exposure)}
|
||||||
|
/>
|
||||||
|
<DataPoint
|
||||||
|
label="Hours/Day"
|
||||||
|
value={`${environment.lighting.naturalLight.hoursPerDay}h`}
|
||||||
|
/>
|
||||||
|
<DataPoint
|
||||||
|
label="Direction"
|
||||||
|
value={formatValue(environment.lighting.naturalLight.direction)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{environment.lighting.artificialLight && (
|
||||||
|
<>
|
||||||
|
<DataPoint
|
||||||
|
label="Light Type"
|
||||||
|
value={formatValue(environment.lighting.artificialLight.type)}
|
||||||
|
/>
|
||||||
|
<DataPoint
|
||||||
|
label="Hours/Day"
|
||||||
|
value={`${environment.lighting.artificialLight.hoursPerDay}h`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location & Container */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">📍 Location</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<DataPoint label="Type" value={formatValue(environment.location.type)} />
|
||||||
|
{environment.location.room && (
|
||||||
|
<DataPoint label="Room" value={environment.location.room} />
|
||||||
|
)}
|
||||||
|
{environment.location.elevation && (
|
||||||
|
<DataPoint label="Elevation" value={`${environment.location.elevation}m`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{environment.container && (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">🪴 Container</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<DataPoint label="Type" value={formatValue(environment.container.type)} />
|
||||||
|
{environment.container.material && (
|
||||||
|
<DataPoint label="Material" value={formatValue(environment.container.material)} />
|
||||||
|
)}
|
||||||
|
{environment.container.size && (
|
||||||
|
<DataPoint label="Size" value={environment.container.size} />
|
||||||
|
)}
|
||||||
|
<DataPoint
|
||||||
|
label="Drainage"
|
||||||
|
value={environment.container.drainage === 'yes' ? '✓ Yes' : '✗ No'}
|
||||||
|
highlight={environment.container.drainage === 'no' ? 'red' : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Watering */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">💧 Watering</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
<DataPoint label="Method" value={formatValue(environment.watering.method)} />
|
||||||
|
<DataPoint label="Source" value={formatValue(environment.watering.waterSource)} />
|
||||||
|
{environment.watering.frequency && (
|
||||||
|
<DataPoint label="Frequency" value={environment.watering.frequency} />
|
||||||
|
)}
|
||||||
|
{environment.watering.waterQuality?.pH && (
|
||||||
|
<DataPoint label="Water pH" value={environment.watering.waterQuality.pH.toFixed(1)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nutrients */}
|
||||||
|
{environment.nutrients && (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">🧪 Nutrients</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
<DataPoint
|
||||||
|
label="NPK"
|
||||||
|
value={`${environment.nutrients.nitrogen}-${environment.nutrients.phosphorus}-${environment.nutrients.potassium}`}
|
||||||
|
/>
|
||||||
|
{environment.nutrients.ec && (
|
||||||
|
<DataPoint label="EC" value={`${environment.nutrients.ec} mS/cm`} />
|
||||||
|
)}
|
||||||
|
{environment.nutrients.tds && (
|
||||||
|
<DataPoint label="TDS" value={`${environment.nutrients.tds} ppm`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Surroundings */}
|
||||||
|
{environment.surroundings && (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">🌿 Surroundings</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{environment.surroundings.ecosystem && (
|
||||||
|
<DataPoint label="Ecosystem" value={formatValue(environment.surroundings.ecosystem)} />
|
||||||
|
)}
|
||||||
|
{environment.surroundings.windExposure && (
|
||||||
|
<DataPoint label="Wind" value={formatValue(environment.surroundings.windExposure)} />
|
||||||
|
)}
|
||||||
|
{environment.surroundings.companionPlants && environment.surroundings.companionPlants.length > 0 && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<p className="text-sm font-medium text-gray-700">Companion Plants:</p>
|
||||||
|
<p className="text-sm text-gray-900">{environment.surroundings.companionPlants.join(', ')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecommendationCard({ recommendation }: { recommendation: EnvironmentalRecommendation }) {
|
||||||
|
const priorityColors = {
|
||||||
|
critical: 'border-red-500 bg-red-50',
|
||||||
|
high: 'border-orange-500 bg-orange-50',
|
||||||
|
medium: 'border-yellow-500 bg-yellow-50',
|
||||||
|
low: 'border-blue-500 bg-blue-50',
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityIcons = {
|
||||||
|
critical: '🚨',
|
||||||
|
high: '⚠️',
|
||||||
|
medium: '💡',
|
||||||
|
low: 'ℹ️',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`border-l-4 p-4 rounded-r-lg ${priorityColors[recommendation.priority]}`}>
|
||||||
|
<div className="flex items-start">
|
||||||
|
<span className="text-2xl mr-3">{priorityIcons[recommendation.priority]}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<h4 className="font-semibold text-gray-900">{recommendation.issue}</h4>
|
||||||
|
<span className="text-xs uppercase font-semibold text-gray-600">
|
||||||
|
{recommendation.priority}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-800 mb-2">
|
||||||
|
<strong>Recommendation:</strong> {recommendation.recommendation}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-700">
|
||||||
|
<strong>Impact:</strong> {recommendation.impact}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DataPoint({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
highlight,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
highlight?: 'red' | 'green';
|
||||||
|
}) {
|
||||||
|
const highlightClass = highlight === 'red' ? 'text-red-600 font-semibold' : highlight === 'green' ? 'text-green-600 font-semibold' : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-600 mb-1">{label}</p>
|
||||||
|
<p className={`text-sm font-medium text-gray-900 ${highlightClass}`}>{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(value: string): string {
|
||||||
|
return value.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScoreColor(score: number): string {
|
||||||
|
if (score >= 90) return 'text-green-600';
|
||||||
|
if (score >= 75) return 'text-lime-600';
|
||||||
|
if (score >= 60) return 'text-yellow-600';
|
||||||
|
if (score >= 40) return 'text-orange-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScoreBgColor(score: number): string {
|
||||||
|
if (score >= 90) return 'bg-green-600';
|
||||||
|
if (score >= 75) return 'bg-lime-600';
|
||||||
|
if (score >= 60) return 'bg-yellow-600';
|
||||||
|
if (score >= 40) return 'bg-orange-600';
|
||||||
|
return 'bg-red-600';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScoreInterpretation(score: number): string {
|
||||||
|
if (score >= 90) return '🌟 Excellent conditions - your plant should thrive!';
|
||||||
|
if (score >= 75) return '✓ Good conditions with minor room for improvement';
|
||||||
|
if (score >= 60) return '⚡ Adequate conditions, several optimization opportunities';
|
||||||
|
if (score >= 40) return '⚠️ Suboptimal conditions, address high-priority issues';
|
||||||
|
return '🚨 Poor conditions, immediate action needed';
|
||||||
|
}
|
||||||
708
components/EnvironmentalForm.tsx
Normal file
708
components/EnvironmentalForm.tsx
Normal file
|
|
@ -0,0 +1,708 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
GrowingEnvironment,
|
||||||
|
SoilComposition,
|
||||||
|
ClimateConditions,
|
||||||
|
LightingConditions,
|
||||||
|
NutrientProfile,
|
||||||
|
WateringSchedule,
|
||||||
|
ContainerInfo,
|
||||||
|
EnvironmentLocation,
|
||||||
|
SurroundingEnvironment,
|
||||||
|
} from '../lib/environment/types';
|
||||||
|
|
||||||
|
interface EnvironmentalFormProps {
|
||||||
|
value: Partial<GrowingEnvironment>;
|
||||||
|
onChange: (env: Partial<GrowingEnvironment>) => void;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EnvironmentalForm({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
compact = false,
|
||||||
|
}: EnvironmentalFormProps) {
|
||||||
|
const [activeSection, setActiveSection] = useState<string>('soil');
|
||||||
|
|
||||||
|
const updateSection = <K extends keyof GrowingEnvironment>(
|
||||||
|
section: K,
|
||||||
|
updates: Partial<GrowingEnvironment[K]>
|
||||||
|
) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[section]: {
|
||||||
|
...value[section],
|
||||||
|
...updates,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{ id: 'soil', name: '🌱 Soil', icon: '🌱' },
|
||||||
|
{ id: 'nutrients', name: '🧪 Nutrients', icon: '🧪' },
|
||||||
|
{ id: 'lighting', name: '☀️ Light', icon: '☀️' },
|
||||||
|
{ id: 'climate', name: '🌡️ Climate', icon: '🌡️' },
|
||||||
|
{ id: 'location', name: '📍 Location', icon: '📍' },
|
||||||
|
{ id: 'container', name: '🪴 Container', icon: '🪴' },
|
||||||
|
{ id: 'watering', name: '💧 Water', icon: '💧' },
|
||||||
|
{ id: 'surroundings', name: '🌿 Surroundings', icon: '🌿' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg">
|
||||||
|
{/* Section Tabs */}
|
||||||
|
<div className="flex overflow-x-auto border-b border-gray-200 bg-gray-50">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
onClick={() => setActiveSection(section.id)}
|
||||||
|
className={`px-4 py-3 text-sm font-medium whitespace-nowrap transition ${
|
||||||
|
activeSection === section.id
|
||||||
|
? 'border-b-2 border-green-600 text-green-600 bg-white'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-2">{section.icon}</span>
|
||||||
|
{section.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
{activeSection === 'soil' && (
|
||||||
|
<SoilSection
|
||||||
|
value={value.soil}
|
||||||
|
onChange={(soil) => updateSection('soil', soil)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'nutrients' && (
|
||||||
|
<NutrientsSection
|
||||||
|
value={value.nutrients}
|
||||||
|
onChange={(nutrients) => updateSection('nutrients', nutrients)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'lighting' && (
|
||||||
|
<LightingSection
|
||||||
|
value={value.lighting}
|
||||||
|
onChange={(lighting) => updateSection('lighting', lighting)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'climate' && (
|
||||||
|
<ClimateSection
|
||||||
|
value={value.climate}
|
||||||
|
onChange={(climate) => updateSection('climate', climate)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'location' && (
|
||||||
|
<LocationSection
|
||||||
|
value={value.location}
|
||||||
|
onChange={(location) => updateSection('location', location)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'container' && (
|
||||||
|
<ContainerSection
|
||||||
|
value={value.container}
|
||||||
|
onChange={(container) => updateSection('container', container)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'watering' && (
|
||||||
|
<WateringSection
|
||||||
|
value={value.watering}
|
||||||
|
onChange={(watering) => updateSection('watering', watering)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'surroundings' && (
|
||||||
|
<SurroundingsSection
|
||||||
|
value={value.surroundings}
|
||||||
|
onChange={(surroundings) => updateSection('surroundings', surroundings)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soil Section Component
|
||||||
|
function SoilSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<SoilComposition>;
|
||||||
|
onChange: (soil: Partial<SoilComposition>) => void;
|
||||||
|
}) {
|
||||||
|
const soil = value || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Soil Composition</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Soil Type *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={soil.type || 'loam'}
|
||||||
|
onChange={(e) => onChange({ ...soil, type: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="clay">Clay</option>
|
||||||
|
<option value="sand">Sand</option>
|
||||||
|
<option value="silt">Silt</option>
|
||||||
|
<option value="loam">Loam (balanced)</option>
|
||||||
|
<option value="peat">Peat</option>
|
||||||
|
<option value="chalk">Chalk</option>
|
||||||
|
<option value="custom">Custom Mix</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Soil pH * <span className="text-xs text-gray-500">(most plants: 6.0-7.0)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
max="14"
|
||||||
|
value={soil.pH || 6.5}
|
||||||
|
onChange={(e) => onChange({ ...soil, pH: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
<div className="mt-1 text-xs text-gray-600">
|
||||||
|
{soil.pH && soil.pH < 5.5 && '⚠️ Acidic - may need lime'}
|
||||||
|
{soil.pH && soil.pH >= 5.5 && soil.pH <= 7.5 && '✓ Good range'}
|
||||||
|
{soil.pH && soil.pH > 7.5 && '⚠️ Alkaline - may need sulfur'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Texture</label>
|
||||||
|
<select
|
||||||
|
value={soil.texture || 'medium'}
|
||||||
|
onChange={(e) => onChange({ ...soil, texture: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="heavy">Heavy (clay-rich)</option>
|
||||||
|
<option value="medium">Medium (balanced)</option>
|
||||||
|
<option value="light">Light (sandy)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Drainage</label>
|
||||||
|
<select
|
||||||
|
value={soil.drainage || 'good'}
|
||||||
|
onChange={(e) => onChange({ ...soil, drainage: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="poor">Poor (stays wet)</option>
|
||||||
|
<option value="moderate">Moderate</option>
|
||||||
|
<option value="good">Good</option>
|
||||||
|
<option value="excellent">Excellent (fast draining)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Organic Matter % <span className="text-xs text-gray-500">(ideal: 5-10%)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={soil.organicMatter || 5}
|
||||||
|
onChange={(e) => onChange({ ...soil, organicMatter: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
|
💡 <strong>Tip:</strong> Test soil pH with a meter or test kit for accuracy. Most vegetables prefer pH 6.0-7.0.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nutrients Section Component
|
||||||
|
function NutrientsSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<NutrientProfile>;
|
||||||
|
onChange: (nutrients: Partial<NutrientProfile>) => void;
|
||||||
|
}) {
|
||||||
|
const nutrients = value || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Nutrient Profile</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 mb-3">Primary Nutrients (NPK)</h4>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nitrogen (N) %
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
value={nutrients.nitrogen || 0}
|
||||||
|
onChange={(e) => onChange({ ...nutrients, nitrogen: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 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">
|
||||||
|
Phosphorus (P) %
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
value={nutrients.phosphorus || 0}
|
||||||
|
onChange={(e) => onChange({ ...nutrients, phosphorus: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 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">
|
||||||
|
Potassium (K) %
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
value={nutrients.potassium || 0}
|
||||||
|
onChange={(e) => onChange({ ...nutrients, potassium: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs text-gray-600">
|
||||||
|
NPK ratio: {nutrients.nitrogen || 0}-{nutrients.phosphorus || 0}-{nutrients.potassium || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
💡 <strong>Tip:</strong> Leave at 0 if unknown. Use soil test kit for accurate NPK values.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lighting Section Component
|
||||||
|
function LightingSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<LightingConditions>;
|
||||||
|
onChange: (lighting: Partial<LightingConditions>) => void;
|
||||||
|
}) {
|
||||||
|
const lighting = value || { type: 'natural' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Lighting Conditions</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Light Type</label>
|
||||||
|
<select
|
||||||
|
value={lighting.type || 'natural'}
|
||||||
|
onChange={(e) => onChange({ ...lighting, type: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="natural">Natural Sunlight</option>
|
||||||
|
<option value="artificial">Artificial Light Only</option>
|
||||||
|
<option value="mixed">Mixed (Natural + Artificial)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(lighting.type === 'natural' || lighting.type === 'mixed') && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-yellow-50 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Sun Exposure
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={lighting.naturalLight?.exposure || 'full_sun'}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...lighting,
|
||||||
|
naturalLight: {
|
||||||
|
...lighting.naturalLight,
|
||||||
|
exposure: e.target.value as any,
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="full_sun">Full Sun (6+ hours)</option>
|
||||||
|
<option value="partial_sun">Partial Sun (4-6 hours)</option>
|
||||||
|
<option value="partial_shade">Partial Shade (2-4 hours)</option>
|
||||||
|
<option value="full_shade">Full Shade (<2 hours)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Hours of Sunlight/Day
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="24"
|
||||||
|
value={lighting.naturalLight?.hoursPerDay || 8}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...lighting,
|
||||||
|
naturalLight: {
|
||||||
|
...lighting.naturalLight,
|
||||||
|
hoursPerDay: parseInt(e.target.value),
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
💡 <strong>Tip:</strong> Most vegetables need 6+ hours of direct sunlight. Herbs can do well with 4-6 hours.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Climate Section Component
|
||||||
|
function ClimateSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<ClimateConditions>;
|
||||||
|
onChange: (climate: Partial<ClimateConditions>) => void;
|
||||||
|
}) {
|
||||||
|
const climate = value || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Climate Conditions</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Day Temperature (°C) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={climate.temperatureDay || 22}
|
||||||
|
onChange={(e) => onChange({ ...climate, temperatureDay: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 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">
|
||||||
|
Night Temperature (°C) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={climate.temperatureNight || 18}
|
||||||
|
onChange={(e) => onChange({ ...climate, temperatureNight: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 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">
|
||||||
|
Average Humidity (%) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={climate.humidityAverage || 50}
|
||||||
|
onChange={(e) => onChange({ ...climate, humidityAverage: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 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">Airflow</label>
|
||||||
|
<select
|
||||||
|
value={climate.airflow || 'moderate'}
|
||||||
|
onChange={(e) => onChange({ ...climate, airflow: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="none">None (still air)</option>
|
||||||
|
<option value="minimal">Minimal</option>
|
||||||
|
<option value="moderate">Moderate (good)</option>
|
||||||
|
<option value="strong">Strong/Windy</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
💡 <strong>Tip:</strong> Most plants thrive at 18-25°C. Good airflow prevents disease.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location Section Component
|
||||||
|
function LocationSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<EnvironmentLocation>;
|
||||||
|
onChange: (location: Partial<EnvironmentLocation>) => void;
|
||||||
|
}) {
|
||||||
|
const location = value || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Growing Location</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Location Type *</label>
|
||||||
|
<select
|
||||||
|
value={location.type || 'outdoor'}
|
||||||
|
onChange={(e) => onChange({ ...location, type: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="indoor">Indoor</option>
|
||||||
|
<option value="outdoor">Outdoor</option>
|
||||||
|
<option value="greenhouse">Greenhouse</option>
|
||||||
|
<option value="polytunnel">Polytunnel</option>
|
||||||
|
<option value="shade_house">Shade House</option>
|
||||||
|
<option value="window">Window</option>
|
||||||
|
<option value="balcony">Balcony</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
💡 <strong>Tip:</strong> Location type affects climate control and pest exposure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container Section Component
|
||||||
|
function ContainerSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<ContainerInfo>;
|
||||||
|
onChange: (container: Partial<ContainerInfo>) => void;
|
||||||
|
}) {
|
||||||
|
const container = value || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Container Information</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Container Type</label>
|
||||||
|
<select
|
||||||
|
value={container.type || 'pot'}
|
||||||
|
onChange={(e) => onChange({ ...container, type: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="pot">Pot</option>
|
||||||
|
<option value="raised_bed">Raised Bed</option>
|
||||||
|
<option value="ground">In Ground</option>
|
||||||
|
<option value="fabric_pot">Fabric Pot</option>
|
||||||
|
<option value="hydroponic">Hydroponic</option>
|
||||||
|
<option value="hanging_basket">Hanging Basket</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Drainage * <span className="text-xs text-red-600">(CRITICAL!)</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={container.drainage || 'yes'}
|
||||||
|
onChange={(e) => onChange({ ...container, drainage: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="yes">Yes (has holes)</option>
|
||||||
|
<option value="no">No (sealed)</option>
|
||||||
|
</select>
|
||||||
|
{container.drainage === 'no' && (
|
||||||
|
<p className="mt-1 text-xs text-red-600 font-semibold">
|
||||||
|
⚠️ WARNING: No drainage will likely kill your plant!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Size (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g., 5 gallon, 30cm diameter"
|
||||||
|
value={container.size || ''}
|
||||||
|
onChange={(e) => onChange({ ...container, size: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
💡 <strong>Tip:</strong> Always ensure drainage! Sitting water = root rot = dead plant.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watering Section Component
|
||||||
|
function WateringSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<WateringSchedule>;
|
||||||
|
onChange: (watering: Partial<WateringSchedule>) => void;
|
||||||
|
}) {
|
||||||
|
const watering = value || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Watering Schedule</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Method</label>
|
||||||
|
<select
|
||||||
|
value={watering.method || 'hand_water'}
|
||||||
|
onChange={(e) => onChange({ ...watering, method: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="hand_water">Hand Watering</option>
|
||||||
|
<option value="drip">Drip Irrigation</option>
|
||||||
|
<option value="soaker_hose">Soaker Hose</option>
|
||||||
|
<option value="sprinkler">Sprinkler</option>
|
||||||
|
<option value="self_watering">Self-Watering</option>
|
||||||
|
<option value="rain">Rain Fed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Water Source</label>
|
||||||
|
<select
|
||||||
|
value={watering.waterSource || 'tap'}
|
||||||
|
onChange={(e) => onChange({ ...watering, waterSource: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="tap">Tap Water</option>
|
||||||
|
<option value="well">Well Water</option>
|
||||||
|
<option value="rain">Rain Water</option>
|
||||||
|
<option value="filtered">Filtered</option>
|
||||||
|
<option value="distilled">Distilled</option>
|
||||||
|
<option value="RO">Reverse Osmosis (RO)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Frequency (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g., daily, every 2-3 days"
|
||||||
|
value={watering.frequency || ''}
|
||||||
|
onChange={(e) => onChange({ ...watering, frequency: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
💡 <strong>Tip:</strong> Water when top inch of soil is dry. Overwatering kills more plants than underwatering!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surroundings Section Component
|
||||||
|
function SurroundingsSection({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: Partial<SurroundingEnvironment>;
|
||||||
|
onChange: (surroundings: Partial<SurroundingEnvironment>) => void;
|
||||||
|
}) {
|
||||||
|
const surroundings = value || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Surrounding Environment</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ecosystem Type</label>
|
||||||
|
<select
|
||||||
|
value={surroundings.ecosystem || 'urban'}
|
||||||
|
onChange={(e) => onChange({ ...surroundings, ecosystem: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="urban">Urban</option>
|
||||||
|
<option value="suburban">Suburban</option>
|
||||||
|
<option value="rural">Rural</option>
|
||||||
|
<option value="forest">Forest</option>
|
||||||
|
<option value="desert">Desert</option>
|
||||||
|
<option value="coastal">Coastal</option>
|
||||||
|
<option value="mountain">Mountain</option>
|
||||||
|
<option value="tropical">Tropical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Wind Exposure</label>
|
||||||
|
<select
|
||||||
|
value={surroundings.windExposure || 'moderate'}
|
||||||
|
onChange={(e) => onChange({ ...surroundings, windExposure: e.target.value as any })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="sheltered">Sheltered (protected)</option>
|
||||||
|
<option value="moderate">Moderate</option>
|
||||||
|
<option value="exposed">Exposed</option>
|
||||||
|
<option value="windy">Very Windy</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
💡 <strong>Tip:</strong> Track companion plants and pests to learn what works in your ecosystem.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import EnvironmentalDisplay from '../../components/EnvironmentalDisplay';
|
||||||
|
import { GrowingEnvironment } from '../../lib/environment/types';
|
||||||
|
|
||||||
interface Plant {
|
interface Plant {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -28,6 +30,7 @@ interface Plant {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
childPlants: string[];
|
childPlants: string[];
|
||||||
|
environment?: GrowingEnvironment;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
registeredAt: string;
|
registeredAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
@ -48,7 +51,7 @@ export default function PlantDetail() {
|
||||||
const [lineage, setLineage] = useState<Lineage | null>(null);
|
const [lineage, setLineage] = useState<Lineage | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [activeTab, setActiveTab] = useState<'details' | 'lineage'>('details');
|
const [activeTab, setActiveTab] = useState<'details' | 'lineage' | 'environment'>('details');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|
@ -219,6 +222,21 @@ export default function PlantDetail() {
|
||||||
>
|
>
|
||||||
🌳 Family Tree
|
🌳 Family Tree
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('environment')}
|
||||||
|
className={`px-6 py-3 rounded-lg font-semibold transition ${
|
||||||
|
activeTab === 'environment'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-white text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🌍 Environment
|
||||||
|
{!plant.environment && (
|
||||||
|
<span className="ml-2 px-2 py-0.5 bg-yellow-500 text-white text-xs rounded-full">
|
||||||
|
Add
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Details Tab */}
|
{/* Details Tab */}
|
||||||
|
|
@ -362,6 +380,69 @@ export default function PlantDetail() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Environment Tab */}
|
||||||
|
{activeTab === 'environment' && (
|
||||||
|
<div>
|
||||||
|
{plant.environment ? (
|
||||||
|
<EnvironmentalDisplay
|
||||||
|
environment={plant.environment}
|
||||||
|
plantId={plant.id}
|
||||||
|
showRecommendations={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-12 text-center">
|
||||||
|
<div className="text-6xl mb-4">🌍</div>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
|
No Environmental Data Yet
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-lg mb-6">
|
||||||
|
Track soil, climate, nutrients, and growing conditions to:
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-4xl mx-auto mb-8">
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
|
<div className="text-3xl mb-2">💡</div>
|
||||||
|
<h4 className="font-semibold text-gray-900 mb-1">Get Recommendations</h4>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Receive personalized advice to optimize conditions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<div className="text-3xl mb-2">🔍</div>
|
||||||
|
<h4 className="font-semibold text-gray-900 mb-1">Compare & Learn</h4>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Find what works for similar plants
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-50 p-4 rounded-lg">
|
||||||
|
<div className="text-3xl mb-2">📈</div>
|
||||||
|
<h4 className="font-semibold text-gray-900 mb-1">Track Success</h4>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Monitor growth and health over time
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 mb-6">
|
||||||
|
📘 Learn more in the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/yourusername/localgreenchain/blob/main/ENVIRONMENTAL_TRACKING.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-green-600 hover:underline"
|
||||||
|
>
|
||||||
|
Environmental Tracking Guide
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => alert('Environmental data editing coming soon! Use API for now.')}
|
||||||
|
className="px-6 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition"
|
||||||
|
>
|
||||||
|
Add Environmental Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue