feat: Sprint 6 - Final Polish
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
Deploy to Production / deploy (push) Waiting to run

- PlantDataCard.tsx: Deterministic mock data seeding via plant ID
- Added Batch ID, Batch Name, and Last Updated timestamp
- Improved sparkline history generation for realism
- Enhanced UI with glassmorphic refinements and animations
- Contextual actions styling
This commit is contained in:
fullsizemalt 2025-12-19 13:53:19 -08:00
parent 0c05c41e81
commit f033427002

View file

@ -1,4 +1,5 @@
import { X, MapPin, Leaf, Activity, Calendar } from 'lucide-react'; import { X, MapPin, Leaf, Activity, Calendar, Box, Clock } from 'lucide-react';
import { useMemo } from 'react';
import type { PlantPosition } from './types'; import type { PlantPosition } from './types';
import { LifecycleTimeline } from './LifecycleTimeline'; import { LifecycleTimeline } from './LifecycleTimeline';
import { VitalsGrid } from './VitalGauge'; import { VitalsGrid } from './VitalGauge';
@ -8,22 +9,56 @@ interface PlantDataCardProps {
onClose: () => void; onClose: () => void;
} }
// Mock vitals for demo - now with consistent values per plant /**
* Mock vitals for demo - now with consistent values and history per plant.
* Seeding by plantId ensures that clicking the same plant twice shows the same data,
* and different plants show unique, deterministic data.
*/
const getMockVitals = (plantId?: string) => { const getMockVitals = (plantId?: string) => {
// Use plant ID to seed pseudo-random values for consistency // Generate a deterministic seed from the plant ID
const seed = plantId ? plantId.charCodeAt(plantId.length - 1) : 42; const seed = plantId ? plantId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) : 42;
// Generate deterministic history based on seed
const generateHistory = (base: number, variance: number, points: number = 12) => {
const history: number[] = [];
for (let i = 0; i < points; i++) {
// Using Math.sin with the seed to create a deterministic "wavy" history
const noise = Math.sin(seed + i * 0.8) * variance;
history.push(base + noise);
}
return history;
};
const healthBase = 88 + (seed % 10);
const tempBase = 74 + (seed % 4);
const humBase = 58 + (seed % 8);
const vpdBase = 1.05 + (seed % 5) / 20;
return { return {
health: 75 + (seed % 20), health: healthBase,
temperature: 72 + (seed % 8), healthHistory: generateHistory(healthBase, 2),
humidity: 55 + ((seed * 7) % 15), temperature: tempBase,
vpd: 1.0 + ((seed % 10) / 25), tempHistory: generateHistory(tempBase, 1.5),
humidity: humBase,
humHistory: generateHistory(humBase, 4),
vpd: vpdBase,
vpdHistory: generateHistory(vpdBase, 0.15),
batch: `B-${(seed % 9999).toString(16).toUpperCase().padStart(4, '0')}`,
batchName: `Batch ${(seed % 150) + 101}`,
lastUpdated: new Date(Date.now() - (seed % 45) * 60 * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
}; };
}; };
export function PlantDataCard({ plant, onClose }: PlantDataCardProps) { export function PlantDataCard({ plant, onClose }: PlantDataCardProps) {
if (!plant?.plant) return null; // Early escape and null safety
if (!plant || !plant.plant) {
return null;
}
const vitals = getMockVitals(plant.plant.id); const { plant: plantInfo } = plant;
// Memoize vitals based on plant ID to prevent re-rolls on every render
const vitals = useMemo(() => getMockVitals(plantInfo.id), [plantInfo.id]);
// Format position breadcrumb // Format position breadcrumb
const position = plant.breadcrumb const position = plant.breadcrumb
@ -31,21 +66,24 @@ export function PlantDataCard({ plant, onClose }: PlantDataCardProps) {
: `R${plant.row} • C${plant.column} • T${plant.tier}`; : `R${plant.row} • C${plant.column} • T${plant.tier}`;
return ( return (
<div className="bg-slate-900/95 backdrop-blur-md border border-slate-700/50 rounded-lg shadow-2xl w-80 overflow-hidden"> <div className="bg-slate-900/95 backdrop-blur-md border border-slate-700/50 rounded-lg shadow-2xl w-80 overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-300">
{/* Header */} {/* Header */}
<div className="bg-gradient-to-r from-slate-800 to-slate-900 px-4 py-3 flex items-center justify-between border-b border-slate-700/50"> <div className="bg-gradient-to-r from-slate-800 to-slate-900 px-4 py-3 flex items-center justify-between border-b border-slate-700/50">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-mono text-cyan-400 truncate"> <span className="text-sm font-mono text-cyan-400 truncate">
{plant.plant.tagNumber} {plantInfo.tagNumber}
</span> </span>
<span className="px-2 py-0.5 text-[10px] font-bold uppercase bg-purple-600/30 text-purple-300 rounded"> <span className={`px-2 py-0.5 text-[10px] font-bold uppercase rounded ${plantInfo.stage === 'FLOWERING' ? 'bg-purple-600/30 text-purple-300' :
{plant.plant.stage?.replace('_', ' ') || 'VEG'} plantInfo.stage === 'VEGETATIVE' ? 'bg-green-600/30 text-green-300' :
'bg-blue-600/30 text-blue-300'
}`}>
{plantInfo.stage?.replace('_', ' ') || 'VEG'}
</span> </span>
</div> </div>
<div className="text-xs text-slate-400 mt-0.5 flex items-center gap-1"> <div className="text-xs text-slate-400 mt-0.5 flex items-center gap-1">
<Leaf className="w-3 h-3" /> <Leaf className="w-3 h-3" />
{plant.plant.strain || 'Unknown Strain'} {plantInfo.strain || 'Unknown Strain'}
</div> </div>
</div> </div>
<button <button
@ -56,28 +94,40 @@ export function PlantDataCard({ plant, onClose }: PlantDataCardProps) {
</button> </button>
</div> </div>
{/* Position */} {/* Position & Batch Metadata */}
<div className="px-4 py-2 bg-slate-800/50 border-b border-slate-700/30"> <div className="px-4 py-2 bg-slate-800/40 border-b border-slate-700/30 space-y-1">
<div className="flex items-center gap-1.5 text-xs text-slate-300"> <div className="flex items-center gap-1.5 text-[10px] text-slate-300">
<MapPin className="w-3 h-3 text-slate-400" /> <MapPin className="w-3 h-3 text-slate-500" />
{position} {position}
</div> </div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[10px] text-slate-400">
<Box className="w-3 h-3 text-slate-500" />
<span className="font-mono text-slate-300">{vitals.batch}</span>
<span></span>
<span className="truncate max-w-[120px]">{vitals.batchName}</span>
</div>
<div className="flex items-center gap-1 text-[9px] text-slate-500">
<Clock className="w-2.5 h-2.5" />
{vitals.lastUpdated}
</div>
</div>
</div> </div>
{/* Lifecycle Timeline */} {/* Lifecycle Timeline */}
<div className="px-4 py-3 border-b border-slate-700/30"> <div className="px-4 py-3 border-b border-slate-700/30">
<div className="text-xs text-slate-400 mb-2 flex items-center gap-1"> <div className="text-xs text-slate-400 mb-2 flex items-center gap-1 uppercase tracking-wider font-semibold">
<Calendar className="w-3 h-3" /> <Calendar className="w-3 h-3" />
LIFECYCLE Lifecycle Timeline
</div> </div>
<LifecycleTimeline currentStage={plant.plant.stage ?? undefined} /> <LifecycleTimeline currentStage={plantInfo.stage ?? undefined} />
</div> </div>
{/* Vitals Grid - Now with sparklines */} {/* Vitals Grid with Sparklines */}
<div className="px-4 py-3 border-b border-slate-700/30"> <div className="px-4 py-3 border-b border-slate-700/30">
<div className="text-xs text-slate-400 mb-2 flex items-center gap-1"> <div className="text-xs text-slate-400 mb-2 flex items-center gap-1 uppercase tracking-wider font-semibold">
<Activity className="w-3 h-3" /> <Activity className="w-3 h-3" />
VITALS Environmental Vitals
</div> </div>
<VitalsGrid <VitalsGrid
health={vitals.health} health={vitals.health}
@ -87,19 +137,18 @@ export function PlantDataCard({ plant, onClose }: PlantDataCardProps) {
/> />
</div> </div>
{/* Quick Actions */} {/* Contextual Actions */}
<div className="px-4 py-3 flex gap-2"> <div className="px-4 py-3 flex gap-2">
<button className="flex-1 px-2 py-1.5 text-xs bg-slate-800 hover:bg-slate-700 border border-slate-600/50 rounded transition-colors"> <button className="flex-1 px-2 py-2 text-xs bg-slate-800 hover:bg-slate-700 border border-slate-600/50 rounded transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-1.5">
📸 Photo 📸 Photo
</button> </button>
<button className="flex-1 px-2 py-1.5 text-xs bg-slate-800 hover:bg-slate-700 border border-slate-600/50 rounded transition-colors"> <button className="flex-1 px-2 py-2 text-xs bg-slate-800 hover:bg-slate-700 border border-slate-600/50 rounded transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-1.5">
📝 Note 📝 Note
</button> </button>
<button className="flex-1 px-2 py-1.5 text-xs bg-slate-800 hover:bg-slate-700 border border-slate-600/50 rounded transition-colors"> <button className="flex-1 px-2 py-2 text-xs bg-cyan-600/10 hover:bg-cyan-600/20 border border-cyan-500/30 rounded text-cyan-400 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-1.5 font-bold">
🔍 Metrc 🔍 Metrc
</button> </button>
</div> </div>
</div> </div>
); );
} }