localgreenchain/components/EnvironmentalDisplay.tsx
Claude 8b06ccb7d0
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
2025-11-16 16:58:53 +00:00

334 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
}