feat: Sprint 6 - Final Polish
- 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:
parent
0c05c41e81
commit
f033427002
1 changed files with 79 additions and 30 deletions
|
|
@ -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 { LifecycleTimeline } from './LifecycleTimeline';
|
||||
import { VitalsGrid } from './VitalGauge';
|
||||
|
|
@ -8,22 +9,56 @@ interface PlantDataCardProps {
|
|||
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) => {
|
||||
// Use plant ID to seed pseudo-random values for consistency
|
||||
const seed = plantId ? plantId.charCodeAt(plantId.length - 1) : 42;
|
||||
// Generate a deterministic seed from the plant ID
|
||||
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 {
|
||||
health: 75 + (seed % 20),
|
||||
temperature: 72 + (seed % 8),
|
||||
humidity: 55 + ((seed * 7) % 15),
|
||||
vpd: 1.0 + ((seed % 10) / 25),
|
||||
health: healthBase,
|
||||
healthHistory: generateHistory(healthBase, 2),
|
||||
temperature: tempBase,
|
||||
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) {
|
||||
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
|
||||
const position = plant.breadcrumb
|
||||
|
|
@ -31,21 +66,24 @@ export function PlantDataCard({ plant, onClose }: PlantDataCardProps) {
|
|||
: `R${plant.row} • C${plant.column} • T${plant.tier}`;
|
||||
|
||||
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 */}
|
||||
<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 items-center gap-2">
|
||||
<span className="text-sm font-mono text-cyan-400 truncate">
|
||||
{plant.plant.tagNumber}
|
||||
{plantInfo.tagNumber}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-[10px] font-bold uppercase bg-purple-600/30 text-purple-300 rounded">
|
||||
{plant.plant.stage?.replace('_', ' ') || 'VEG'}
|
||||
<span className={`px-2 py-0.5 text-[10px] font-bold uppercase rounded ${plantInfo.stage === 'FLOWERING' ? 'bg-purple-600/30 text-purple-300' :
|
||||
plantInfo.stage === 'VEGETATIVE' ? 'bg-green-600/30 text-green-300' :
|
||||
'bg-blue-600/30 text-blue-300'
|
||||
}`}>
|
||||
{plantInfo.stage?.replace('_', ' ') || 'VEG'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-0.5 flex items-center gap-1">
|
||||
<Leaf className="w-3 h-3" />
|
||||
{plant.plant.strain || 'Unknown Strain'}
|
||||
{plantInfo.strain || 'Unknown Strain'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -56,28 +94,40 @@ export function PlantDataCard({ plant, onClose }: PlantDataCardProps) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Position */}
|
||||
<div className="px-4 py-2 bg-slate-800/50 border-b border-slate-700/30">
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-300">
|
||||
<MapPin className="w-3 h-3 text-slate-400" />
|
||||
{/* Position & Batch Metadata */}
|
||||
<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-[10px] text-slate-300">
|
||||
<MapPin className="w-3 h-3 text-slate-500" />
|
||||
{position}
|
||||
</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>
|
||||
|
||||
{/* Lifecycle Timeline */}
|
||||
<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" />
|
||||
LIFECYCLE
|
||||
Lifecycle Timeline
|
||||
</div>
|
||||
<LifecycleTimeline currentStage={plant.plant.stage ?? undefined} />
|
||||
<LifecycleTimeline currentStage={plantInfo.stage ?? undefined} />
|
||||
</div>
|
||||
|
||||
{/* Vitals Grid - Now with sparklines */}
|
||||
{/* Vitals Grid with Sparklines */}
|
||||
<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" />
|
||||
VITALS
|
||||
Environmental Vitals
|
||||
</div>
|
||||
<VitalsGrid
|
||||
health={vitals.health}
|
||||
|
|
@ -87,19 +137,18 @@ export function PlantDataCard({ plant, onClose }: PlantDataCardProps) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{/* Contextual Actions */}
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue