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
491 lines
17 KiB
TypeScript
491 lines
17 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import Link from 'next/link';
|
|
import Head from 'next/head';
|
|
import EnvironmentalDisplay from '../../components/EnvironmentalDisplay';
|
|
import { GrowingEnvironment } from '../../lib/environment/types';
|
|
|
|
interface Plant {
|
|
id: string;
|
|
commonName: string;
|
|
scientificName?: string;
|
|
species?: string;
|
|
genus?: string;
|
|
family?: string;
|
|
parentPlantId?: string;
|
|
propagationType?: string;
|
|
generation: number;
|
|
plantedDate: string;
|
|
status: string;
|
|
location: {
|
|
latitude: number;
|
|
longitude: number;
|
|
city?: string;
|
|
country?: string;
|
|
address?: string;
|
|
};
|
|
owner: {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
};
|
|
childPlants: string[];
|
|
environment?: GrowingEnvironment;
|
|
notes?: string;
|
|
registeredAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface Lineage {
|
|
plantId: string;
|
|
ancestors: Plant[];
|
|
descendants: Plant[];
|
|
siblings: Plant[];
|
|
generation: number;
|
|
}
|
|
|
|
export default function PlantDetail() {
|
|
const router = useRouter();
|
|
const { id } = router.query;
|
|
const [plant, setPlant] = useState<Plant | null>(null);
|
|
const [lineage, setLineage] = useState<Lineage | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [activeTab, setActiveTab] = useState<'details' | 'lineage' | 'environment'>('details');
|
|
|
|
useEffect(() => {
|
|
if (id) {
|
|
fetchPlantData();
|
|
}
|
|
}, [id]);
|
|
|
|
const fetchPlantData = async () => {
|
|
try {
|
|
const [plantResponse, lineageResponse] = await Promise.all([
|
|
fetch(`/api/plants/${id}`),
|
|
fetch(`/api/plants/lineage/${id}`),
|
|
]);
|
|
|
|
const plantData = await plantResponse.json();
|
|
const lineageData = await lineageResponse.json();
|
|
|
|
if (!plantResponse.ok) {
|
|
throw new Error(plantData.error || 'Failed to fetch plant');
|
|
}
|
|
|
|
setPlant(plantData.plant);
|
|
|
|
if (lineageResponse.ok) {
|
|
setLineage(lineageData.lineage);
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.message || 'An error occurred');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-green-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Loading plant data...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !plant) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center">
|
|
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md">
|
|
<h2 className="text-2xl font-bold text-red-600 mb-4">Error</h2>
|
|
<p className="text-gray-700 mb-4">{error || 'Plant not found'}</p>
|
|
<Link href="/">
|
|
<a className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
|
|
Go Home
|
|
</a>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const statusColors: { [key: string]: string } = {
|
|
sprouted: 'bg-yellow-100 text-yellow-800',
|
|
growing: 'bg-green-100 text-green-800',
|
|
mature: 'bg-blue-100 text-blue-800',
|
|
flowering: 'bg-purple-100 text-purple-800',
|
|
fruiting: 'bg-orange-100 text-orange-800',
|
|
dormant: 'bg-gray-100 text-gray-800',
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
<Head>
|
|
<title>
|
|
{plant.commonName} - LocalGreenChain
|
|
</title>
|
|
</Head>
|
|
|
|
{/* Header */}
|
|
<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>
|
|
<nav className="flex gap-4">
|
|
<Link href="/plants/explore">
|
|
<a className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
|
|
Explore
|
|
</a>
|
|
</Link>
|
|
<Link href={`/plants/clone?parentId=${plant.id}`}>
|
|
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
|
|
Clone This Plant
|
|
</a>
|
|
</Link>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
|
{/* Plant Header */}
|
|
<div className="bg-white rounded-lg shadow-xl p-8 mb-6">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div>
|
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
|
{plant.commonName}
|
|
</h1>
|
|
{plant.scientificName && (
|
|
<p className="text-xl italic text-gray-600">
|
|
{plant.scientificName}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<span
|
|
className={`px-4 py-2 rounded-full text-sm font-semibold ${
|
|
statusColors[plant.status] || 'bg-gray-100 text-gray-800'
|
|
}`}
|
|
>
|
|
{plant.status}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
|
|
<div className="bg-green-50 rounded-lg p-4">
|
|
<p className="text-sm text-gray-600">Generation</p>
|
|
<p className="text-2xl font-bold text-green-800">
|
|
{plant.generation}
|
|
</p>
|
|
</div>
|
|
<div className="bg-blue-50 rounded-lg p-4">
|
|
<p className="text-sm text-gray-600">Descendants</p>
|
|
<p className="text-2xl font-bold text-blue-800">
|
|
{plant.childPlants.length}
|
|
</p>
|
|
</div>
|
|
<div className="bg-purple-50 rounded-lg p-4">
|
|
<p className="text-sm text-gray-600">Propagation Type</p>
|
|
<p className="text-2xl font-bold text-purple-800 capitalize">
|
|
{plant.propagationType || 'Original'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex gap-4 mb-6">
|
|
<button
|
|
onClick={() => setActiveTab('details')}
|
|
className={`px-6 py-3 rounded-lg font-semibold transition ${
|
|
activeTab === 'details'
|
|
? 'bg-green-600 text-white'
|
|
: 'bg-white text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
📋 Details
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('lineage')}
|
|
className={`px-6 py-3 rounded-lg font-semibold transition ${
|
|
activeTab === 'lineage'
|
|
? 'bg-green-600 text-white'
|
|
: 'bg-white text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
🌳 Family Tree
|
|
</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>
|
|
|
|
{/* Details Tab */}
|
|
{activeTab === 'details' && (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Plant Information */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
|
Plant Information
|
|
</h2>
|
|
<dl className="space-y-3">
|
|
<InfoRow label="Common Name" value={plant.commonName} />
|
|
{plant.scientificName && (
|
|
<InfoRow label="Scientific Name" value={plant.scientificName} />
|
|
)}
|
|
{plant.genus && <InfoRow label="Genus" value={plant.genus} />}
|
|
{plant.family && <InfoRow label="Family" value={plant.family} />}
|
|
<InfoRow
|
|
label="Planted Date"
|
|
value={new Date(plant.plantedDate).toLocaleDateString()}
|
|
/>
|
|
<InfoRow label="Status" value={plant.status} />
|
|
<InfoRow
|
|
label="Registered"
|
|
value={new Date(plant.registeredAt).toLocaleDateString()}
|
|
/>
|
|
</dl>
|
|
|
|
{plant.notes && (
|
|
<div className="mt-6">
|
|
<h3 className="font-semibold text-gray-900 mb-2">Notes</h3>
|
|
<p className="text-gray-700 bg-gray-50 p-3 rounded-lg">
|
|
{plant.notes}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Location & Owner */}
|
|
<div className="space-y-6">
|
|
{/* Owner */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
|
Owner
|
|
</h2>
|
|
<dl className="space-y-3">
|
|
<InfoRow label="Name" value={plant.owner.name} />
|
|
<InfoRow label="Email" value={plant.owner.email} />
|
|
</dl>
|
|
</div>
|
|
|
|
{/* Location */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
|
Location
|
|
</h2>
|
|
<dl className="space-y-3">
|
|
{plant.location.city && (
|
|
<InfoRow label="City" value={plant.location.city} />
|
|
)}
|
|
{plant.location.country && (
|
|
<InfoRow label="Country" value={plant.location.country} />
|
|
)}
|
|
<InfoRow
|
|
label="Coordinates"
|
|
value={`${plant.location.latitude.toFixed(4)}, ${plant.location.longitude.toFixed(4)}`}
|
|
/>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Lineage Tab */}
|
|
{activeTab === 'lineage' && lineage && (
|
|
<div className="space-y-6">
|
|
{/* Ancestors */}
|
|
{lineage.ancestors.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
|
🌲 Ancestors ({lineage.ancestors.length})
|
|
</h2>
|
|
<div className="space-y-3">
|
|
{lineage.ancestors.map((ancestor, idx) => (
|
|
<PlantLineageCard
|
|
key={ancestor.id}
|
|
plant={ancestor}
|
|
label={`Generation ${ancestor.generation}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Siblings */}
|
|
{lineage.siblings.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
|
👥 Siblings ({lineage.siblings.length})
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{lineage.siblings.map((sibling) => (
|
|
<PlantLineageCard key={sibling.id} plant={sibling} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Descendants */}
|
|
{lineage.descendants.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
|
🌱 Descendants ({lineage.descendants.length})
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{lineage.descendants.map((descendant) => (
|
|
<PlantLineageCard
|
|
key={descendant.id}
|
|
plant={descendant}
|
|
label={`Gen ${descendant.generation} (${descendant.propagationType})`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{lineage.ancestors.length === 0 &&
|
|
lineage.siblings.length === 0 &&
|
|
lineage.descendants.length === 0 && (
|
|
<div className="bg-white rounded-lg shadow-lg p-12 text-center">
|
|
<p className="text-gray-600 text-lg">
|
|
This plant has no recorded lineage yet.
|
|
<br />
|
|
<Link href={`/plants/clone?parentId=${plant.id}`}>
|
|
<a className="text-green-600 hover:underline font-semibold">
|
|
Create a clone to start building the family tree!
|
|
</a>
|
|
</Link>
|
|
</p>
|
|
</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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div>
|
|
<dt className="text-sm font-medium text-gray-600">{label}</dt>
|
|
<dd className="text-base text-gray-900 capitalize">{value}</dd>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PlantLineageCard({
|
|
plant,
|
|
label,
|
|
}: {
|
|
plant: Plant;
|
|
label?: string;
|
|
}) {
|
|
return (
|
|
<Link href={`/plants/${plant.id}`}>
|
|
<a className="block p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition border border-gray-200">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h4 className="font-semibold text-gray-900">{plant.commonName}</h4>
|
|
{plant.scientificName && (
|
|
<p className="text-sm italic text-gray-600">
|
|
{plant.scientificName}
|
|
</p>
|
|
)}
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
👤 {plant.owner.name}
|
|
</p>
|
|
</div>
|
|
{label && (
|
|
<span className="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-semibold">
|
|
{label}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</a>
|
|
</Link>
|
|
);
|
|
}
|