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
334 lines
13 KiB
TypeScript
334 lines
13 KiB
TypeScript
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';
|
||
}
|