fix: intentional walkthrough UI redesign
- DailyWalkthroughPage: Centered, compact start screen with minimal chrome - Summary: Statistical overview with compact cards - ReservoirChecklist: Single column centered, tighter spacing - IrrigationChecklist: Compact status rows, inline toggles - PlantHealthChecklist: Segmented health control, minimal layout - Layout: Remove theme toggle from desktop sidebar (cleaner)
This commit is contained in:
parent
efb298e119
commit
a2120170b6
5 changed files with 429 additions and 699 deletions
|
|
@ -122,17 +122,9 @@ export default function Layout() {
|
|||
<Sidebar />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto">
|
||||
{/* Theme Toggle */}
|
||||
<div className="px-4 py-3 border-t border-default">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* User Menu */}
|
||||
<div className="p-3 border-t border-default">
|
||||
<div className="mt-auto p-3 border-t border-default">
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { ArrowLeft, ChevronRight, Camera, X, Check, AlertTriangle, Minus, Plus } from 'lucide-react';
|
||||
import { ArrowLeft, ChevronRight, Camera, X, Check, Minus, Plus } from 'lucide-react';
|
||||
|
||||
interface Zone {
|
||||
name: string;
|
||||
|
|
@ -83,206 +83,120 @@ export default function IrrigationChecklist({ onComplete, onBack, isPhotoRequire
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Header */}
|
||||
<div className="mb-6 md:mb-10">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="btn btn-secondary p-2"
|
||||
aria-label="Back"
|
||||
>
|
||||
<ArrowLeft size={18} />
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-tertiary rounded-md transition-colors">
|
||||
<ArrowLeft size={16} className="text-secondary" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Irrigation System</h1>
|
||||
<p className="text-secondary text-sm mt-0.5">
|
||||
Zone {currentZoneIndex + 1} of {zones.length}
|
||||
</p>
|
||||
<p className="text-xs text-tertiary">Zone {currentZoneIndex + 1}/{zones.length}</p>
|
||||
<h1 className="text-lg font-semibold text-primary">{currentZone.name}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="h-1 bg-tertiary rounded-full overflow-hidden">
|
||||
{/* Progress */}
|
||||
<div className="flex gap-1 mb-6">
|
||||
{zones.map((_, i) => (
|
||||
<div
|
||||
className="h-full bg-accent transition-all duration-normal"
|
||||
style={{ width: `${((currentZoneIndex + 1) / zones.length) * 100}%` }}
|
||||
key={i}
|
||||
className={`h-1 flex-1 rounded-full ${i <= currentZoneIndex ? 'bg-accent' : 'bg-tertiary'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Two Column on Desktop */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
||||
{/* Left Column - Drippers & Status */}
|
||||
<div className="space-y-6">
|
||||
{/* Zone Info Card */}
|
||||
<div className="card p-6 md:p-8">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-4xl mb-3">🚿</div>
|
||||
<h2 className="text-xl font-semibold text-primary">{currentZone.name}</h2>
|
||||
<p className="text-sm text-tertiary">{currentZone.defaultDrippers} drippers total</p>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className={`flex items-center justify-center gap-2 py-3 rounded-md font-medium ${allGood
|
||||
? 'bg-success-muted text-success'
|
||||
: 'bg-warning-muted text-warning'
|
||||
}`}>
|
||||
{allGood ? <Check size={16} /> : <AlertTriangle size={16} />}
|
||||
{allGood ? 'All Systems Operational' : 'Issues Detected'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Drippers Counter */}
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-4">
|
||||
Drippers Working
|
||||
</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="card p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm text-secondary">Drippers</span>
|
||||
{drippersFailed > 0 && (
|
||||
<span className="text-xs text-destructive">{drippersFailed} failed</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<button
|
||||
onClick={() => setDrippersWorking(Math.max(0, drippersWorking - 1))}
|
||||
className="btn btn-secondary w-12 h-12 p-0"
|
||||
className="w-10 h-10 rounded-md bg-tertiary flex items-center justify-center text-secondary hover:bg-secondary transition-colors"
|
||||
>
|
||||
<Minus size={18} />
|
||||
<Minus size={16} />
|
||||
</button>
|
||||
<div className="flex-1 text-center">
|
||||
<div className="text-4xl font-semibold text-primary">{drippersWorking}</div>
|
||||
{drippersFailed > 0 && (
|
||||
<div className="text-sm text-destructive mt-1">{drippersFailed} failed</div>
|
||||
)}
|
||||
<div className="text-center min-w-[80px]">
|
||||
<div className="text-2xl font-semibold text-primary">{drippersWorking}</div>
|
||||
<div className="text-xs text-tertiary">of {currentZone.defaultDrippers}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setDrippersWorking(Math.min(currentZone.defaultDrippers, drippersWorking + 1))}
|
||||
className="btn btn-secondary w-12 h-12 p-0"
|
||||
className="w-10 h-10 rounded-md bg-tertiary flex items-center justify-center text-secondary hover:bg-secondary transition-colors"
|
||||
>
|
||||
<Plus size={18} />
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Toggles - compact */}
|
||||
<div className="card divide-y divide-subtle mb-4">
|
||||
<StatusRow label="Water Flow" value={waterFlow} onChange={setWaterFlow} />
|
||||
<StatusRow label="Nutrients Mixed" value={nutrientsMixed} onChange={setNutrientsMixed} />
|
||||
<StatusRow label="Schedule Active" value={scheduleActive} onChange={setScheduleActive} />
|
||||
</div>
|
||||
|
||||
{/* Right Column - System Checks */}
|
||||
<div className="space-y-6">
|
||||
{/* System Status Toggles */}
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-4">System Status</label>
|
||||
<div className="space-y-3">
|
||||
<StatusToggle
|
||||
label="Water Flow"
|
||||
value={waterFlow}
|
||||
onChange={setWaterFlow}
|
||||
/>
|
||||
<StatusToggle
|
||||
label="Nutrients Mixed"
|
||||
value={nutrientsMixed}
|
||||
onChange={setNutrientsMixed}
|
||||
/>
|
||||
<StatusToggle
|
||||
label="Schedule Active"
|
||||
value={scheduleActive}
|
||||
onChange={setScheduleActive}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Issues / Notes */}
|
||||
{/* Issues input - only when problems */}
|
||||
{!allGood && (
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-2">Issues / Notes</label>
|
||||
<textarea
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={issues}
|
||||
onChange={(e) => setIssues(e.target.value)}
|
||||
placeholder="Describe any issues found..."
|
||||
className="input w-full resize-none"
|
||||
rows={3}
|
||||
placeholder="Describe issues..."
|
||||
className="input w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Photo Upload */}
|
||||
{/* Photo */}
|
||||
{(isPhotoRequired || !allGood) && (
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-2">
|
||||
{isPhotoRequired ? 'Photo (Required)' : 'Photo (Recommended)'}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="photo-upload"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={(e) => {
|
||||
if (e.target.files?.[0]) {
|
||||
setPhoto(URL.createObjectURL(e.target.files[0]));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
<input type="file" id="photo-upload" className="hidden" accept="image/*" capture="environment"
|
||||
onChange={(e) => { if (e.target.files?.[0]) setPhoto(URL.createObjectURL(e.target.files[0])); }} />
|
||||
{photo ? (
|
||||
<div className="relative">
|
||||
<img src={photo} alt="Preview" className="w-full max-h-48 object-cover rounded-md" />
|
||||
<button
|
||||
onClick={() => setPhoto(null)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-destructive text-white rounded-full shadow-lg"
|
||||
>
|
||||
<X size={14} />
|
||||
<img src={photo} alt="Preview" className="w-full h-24 object-cover rounded-md" />
|
||||
<button onClick={() => setPhoto(null)} className="absolute top-1 right-1 p-1 bg-black/50 rounded-full">
|
||||
<X size={12} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
htmlFor="photo-upload"
|
||||
className={`flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-md cursor-pointer transition-colors ${isPhotoRequired
|
||||
? 'border-destructive/50 bg-destructive-muted hover:border-destructive'
|
||||
: 'border-subtle hover:border-default hover:bg-tertiary'
|
||||
}`}
|
||||
>
|
||||
<Camera size={24} className="text-tertiary mb-2" />
|
||||
<span className="text-sm font-medium text-primary">Tap to capture</span>
|
||||
<span className="text-xs text-tertiary">System status photo</span>
|
||||
<label htmlFor="photo-upload" className={`flex items-center justify-center gap-2 p-3 border border-dashed rounded-md cursor-pointer text-sm ${isPhotoRequired ? 'border-destructive/50 text-destructive' : 'border-subtle text-tertiary'
|
||||
}`}>
|
||||
<Camera size={14} />
|
||||
{isPhotoRequired ? 'Photo required' : 'Add photo'}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<button onClick={onBack} className="btn btn-secondary flex-1 h-12 md:h-14">
|
||||
Back
|
||||
{/* Action */}
|
||||
<button onClick={handleNext} className="btn btn-primary w-full">
|
||||
{isLastZone ? 'Complete' : 'Next'}
|
||||
{!isLastZone && <ChevronRight size={14} />}
|
||||
</button>
|
||||
<button onClick={handleNext} className="btn btn-primary flex-1 h-12 md:h-14">
|
||||
{isLastZone ? 'Complete' : 'Next Zone'}
|
||||
{!isLastZone && <ChevronRight size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Status Toggle Component
|
||||
function StatusToggle({
|
||||
label,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
label: string;
|
||||
value: boolean;
|
||||
onChange: (val: boolean) => void;
|
||||
}) {
|
||||
function StatusRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onChange(!value)}
|
||||
className={`w-full flex items-center justify-between p-4 rounded-md border transition-colors ${value
|
||||
? 'bg-success-muted border-success/30 text-success'
|
||||
: 'bg-destructive-muted border-destructive/30 text-destructive'
|
||||
}`}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-tertiary transition-colors"
|
||||
>
|
||||
<span className="font-medium text-primary">{label}</span>
|
||||
<span className="text-lg">{value ? '✓' : '✗'}</span>
|
||||
<span className="text-sm text-primary">{label}</span>
|
||||
<div className={`w-5 h-5 rounded flex items-center justify-center ${value ? 'bg-success text-white' : 'bg-destructive text-white'
|
||||
}`}>
|
||||
{value ? <Check size={12} /> : <X size={12} />}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { ArrowLeft, ChevronRight, Camera, X, Check, Bug, Droplets, Utensils } from 'lucide-react';
|
||||
import { ArrowLeft, ChevronRight, Camera, X, Check, Bug } from 'lucide-react';
|
||||
|
||||
interface PlantHealthCheckData {
|
||||
zoneName: string;
|
||||
|
|
@ -73,232 +73,149 @@ export default function PlantHealthChecklist({ onComplete, onBack, isPhotoRequir
|
|||
}
|
||||
};
|
||||
|
||||
const healthOptions = [
|
||||
{ value: 'GOOD' as const, label: 'Good', emoji: '😊', color: 'bg-success' },
|
||||
{ value: 'FAIR' as const, label: 'Fair', emoji: '😐', color: 'bg-warning' },
|
||||
{ value: 'NEEDS_ATTENTION' as const, label: 'Needs Attention', emoji: '😟', color: 'bg-destructive' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Header */}
|
||||
<div className="mb-6 md:mb-10">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<button onClick={onBack} className="btn btn-secondary p-2" aria-label="Back">
|
||||
<ArrowLeft size={18} />
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-tertiary rounded-md transition-colors">
|
||||
<ArrowLeft size={16} className="text-secondary" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Plant Health Check</h1>
|
||||
<p className="text-secondary text-sm mt-0.5">
|
||||
Zone {currentZoneIndex + 1} of {zones.length}
|
||||
</p>
|
||||
<p className="text-xs text-tertiary">Zone {currentZoneIndex + 1}/{zones.length}</p>
|
||||
<h1 className="text-lg font-semibold text-primary">{currentZone}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="h-1 bg-tertiary rounded-full overflow-hidden">
|
||||
{/* Progress */}
|
||||
<div className="flex gap-1 mb-6">
|
||||
{zones.map((_, i) => (
|
||||
<div
|
||||
className="h-full bg-accent transition-all duration-normal"
|
||||
style={{ width: `${((currentZoneIndex + 1) / zones.length) * 100}%` }}
|
||||
key={i}
|
||||
className={`h-1 flex-1 rounded-full ${i <= currentZoneIndex ? 'bg-accent' : 'bg-tertiary'}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Content - Two Column on Desktop */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
||||
{/* Left Column - Health Status */}
|
||||
<div className="space-y-6">
|
||||
{/* Zone Info */}
|
||||
<div className="card p-6 md:p-8 text-center">
|
||||
<div className="text-4xl mb-3">🌱</div>
|
||||
<h2 className="text-xl font-semibold text-primary">{currentZone}</h2>
|
||||
<p className="text-sm text-tertiary">Quick visual inspection</p>
|
||||
</div>
|
||||
|
||||
{/* Overall Health */}
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-4">Overall Health</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{healthOptions.map((option) => (
|
||||
{/* Health Status - compact segmented control */}
|
||||
<div className="card p-4 mb-4">
|
||||
<p className="text-xs text-tertiary mb-2">Overall Health</p>
|
||||
<div className="flex gap-2">
|
||||
{(['GOOD', 'FAIR', 'NEEDS_ATTENTION'] as const).map(status => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => setHealthStatus(option.value)}
|
||||
className={`p-4 rounded-md border transition-all ${healthStatus === option.value
|
||||
? `${option.color} border-transparent text-white`
|
||||
: 'bg-tertiary border-subtle text-secondary hover:bg-secondary'
|
||||
key={status}
|
||||
onClick={() => setHealthStatus(status)}
|
||||
className={`flex-1 py-2 rounded-md text-xs font-medium transition-colors ${healthStatus === status
|
||||
? status === 'GOOD' ? 'bg-success text-white'
|
||||
: status === 'FAIR' ? 'bg-warning text-white'
|
||||
: 'bg-destructive text-white'
|
||||
: 'bg-tertiary text-secondary hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-1">{option.emoji}</div>
|
||||
<div className="text-xs font-medium">{option.label}</div>
|
||||
{status === 'NEEDS_ATTENTION' ? 'Attention' : status.charAt(0) + status.slice(1).toLowerCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pest Check */}
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-4">Pests Observed?</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="card divide-y divide-subtle mb-4">
|
||||
<button
|
||||
onClick={() => setPestsObserved(false)}
|
||||
className={`p-4 rounded-md border transition-all ${!pestsObserved
|
||||
? 'bg-success border-transparent text-white'
|
||||
: 'bg-tertiary border-subtle text-secondary hover:bg-secondary'
|
||||
}`}
|
||||
onClick={() => setPestsObserved(!pestsObserved)}
|
||||
className="w-full flex items-center justify-between px-4 py-3"
|
||||
>
|
||||
<Check size={20} className="mx-auto mb-1" />
|
||||
<div className="text-sm font-medium">No Pests</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPestsObserved(true)}
|
||||
className={`p-4 rounded-md border transition-all ${pestsObserved
|
||||
? 'bg-destructive border-transparent text-white'
|
||||
: 'bg-tertiary border-subtle text-secondary hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
<Bug size={20} className="mx-auto mb-1" />
|
||||
<div className="text-sm font-medium">Pests Found</div>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Bug size={14} className={pestsObserved ? 'text-destructive' : 'text-tertiary'} />
|
||||
<span className="text-sm text-primary">Pests Observed</span>
|
||||
</div>
|
||||
|
||||
<div className={`w-5 h-5 rounded flex items-center justify-center ${pestsObserved ? 'bg-destructive text-white' : 'bg-success text-white'
|
||||
}`}>
|
||||
{pestsObserved ? '!' : <Check size={12} />}
|
||||
</div>
|
||||
</button>
|
||||
{pestsObserved && (
|
||||
<div className="px-4 py-3">
|
||||
<input
|
||||
type="text"
|
||||
value={pestType}
|
||||
onChange={(e) => setPestType(e.target.value)}
|
||||
placeholder="Type of pest (e.g., spider mites)"
|
||||
className="input w-full mt-3"
|
||||
placeholder="Type of pest..."
|
||||
className="input w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Access & Notes */}
|
||||
<div className="space-y-6">
|
||||
{/* Access Status */}
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-4">Access Status</label>
|
||||
<div className="space-y-3">
|
||||
<AccessToggle
|
||||
icon={Droplets}
|
||||
<div className="card divide-y divide-subtle mb-4">
|
||||
<StatusRow
|
||||
label="Water Access"
|
||||
value={waterAccess === 'OK'}
|
||||
onChange={(val) => setWaterAccess(val ? 'OK' : 'ISSUES')}
|
||||
onChange={(v) => setWaterAccess(v ? 'OK' : 'ISSUES')}
|
||||
/>
|
||||
<AccessToggle
|
||||
icon={Utensils}
|
||||
label="Food/Nutrient Access"
|
||||
<StatusRow
|
||||
label="Nutrient Access"
|
||||
value={foodAccess === 'OK'}
|
||||
onChange={(val) => setFoodAccess(val ? 'OK' : 'ISSUES')}
|
||||
onChange={(v) => setFoodAccess(v ? 'OK' : 'ISSUES')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
{/* Notes - only if issues */}
|
||||
{hasIssues && (
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-2">
|
||||
Notes / Observations
|
||||
</label>
|
||||
<textarea
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Describe any issues..."
|
||||
className="input w-full resize-none"
|
||||
rows={3}
|
||||
placeholder="Notes..."
|
||||
className="input w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Photo Upload */}
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-2">
|
||||
{isPhotoRequired ? 'Photo (Required)' : 'Reference Photo'}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="ref-photo-upload"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={(e) => {
|
||||
if (e.target.files?.[0]) {
|
||||
setReferencePhoto(URL.createObjectURL(e.target.files[0]));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Photo */}
|
||||
{(isPhotoRequired || hasIssues) && (
|
||||
<div className="mb-4">
|
||||
<input type="file" id="photo-upload" className="hidden" accept="image/*" capture="environment"
|
||||
onChange={(e) => { if (e.target.files?.[0]) setReferencePhoto(URL.createObjectURL(e.target.files[0])); }} />
|
||||
{referencePhoto ? (
|
||||
<div className="relative">
|
||||
<img src={referencePhoto} alt="Preview" className="w-full max-h-48 object-cover rounded-md" />
|
||||
<button
|
||||
onClick={() => setReferencePhoto(null)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-destructive text-white rounded-full shadow-lg"
|
||||
>
|
||||
<X size={14} />
|
||||
<img src={referencePhoto} alt="Preview" className="w-full h-24 object-cover rounded-md" />
|
||||
<button onClick={() => setReferencePhoto(null)} className="absolute top-1 right-1 p-1 bg-black/50 rounded-full">
|
||||
<X size={12} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
htmlFor="ref-photo-upload"
|
||||
className={`flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-md cursor-pointer transition-colors ${isPhotoRequired
|
||||
? 'border-destructive/50 bg-destructive-muted hover:border-destructive'
|
||||
: 'border-subtle hover:border-default hover:bg-tertiary'
|
||||
}`}
|
||||
>
|
||||
<Camera size={24} className="text-tertiary mb-2" />
|
||||
<span className="text-sm font-medium text-primary">Tap to capture</span>
|
||||
<span className="text-xs text-tertiary">Reference photo</span>
|
||||
<label htmlFor="photo-upload" className={`flex items-center justify-center gap-2 p-3 border border-dashed rounded-md cursor-pointer text-sm ${isPhotoRequired ? 'border-destructive/50 text-destructive' : 'border-subtle text-tertiary'
|
||||
}`}>
|
||||
<Camera size={14} />
|
||||
{isPhotoRequired ? 'Photo required' : 'Add photo'}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<button onClick={onBack} className="btn btn-secondary flex-1 h-12 md:h-14">
|
||||
Back
|
||||
{/* Action */}
|
||||
<button onClick={handleNext} className="btn btn-primary w-full">
|
||||
{isLastZone ? 'Complete' : 'Next'}
|
||||
{!isLastZone && <ChevronRight size={14} />}
|
||||
</button>
|
||||
<button onClick={handleNext} className="btn btn-primary flex-1 h-12 md:h-14">
|
||||
{isLastZone ? 'Complete' : 'Next Zone'}
|
||||
{!isLastZone && <ChevronRight size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Access Toggle Component
|
||||
function AccessToggle({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
icon: typeof Droplets;
|
||||
label: string;
|
||||
value: boolean;
|
||||
onChange: (val: boolean) => void;
|
||||
}) {
|
||||
function StatusRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onChange(!value)}
|
||||
className={`w-full flex items-center justify-between p-4 rounded-md border transition-colors ${value
|
||||
? 'bg-success-muted border-success/30'
|
||||
: 'bg-destructive-muted border-destructive/30'
|
||||
}`}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-tertiary transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon size={18} className={value ? 'text-success' : 'text-destructive'} />
|
||||
<span className="font-medium text-primary">{label}</span>
|
||||
<span className="text-sm text-primary">{label}</span>
|
||||
<div className={`w-5 h-5 rounded flex items-center justify-center ${value ? 'bg-success text-white' : 'bg-destructive text-white'
|
||||
}`}>
|
||||
{value ? <Check size={12} /> : <X size={12} />}
|
||||
</div>
|
||||
<span className={`text-lg ${value ? 'text-success' : 'text-destructive'}`}>
|
||||
{value ? '✓' : '✗'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { ArrowLeft, ChevronRight, Camera, X, AlertCircle } from 'lucide-react';
|
||||
import { ArrowLeft, ChevronRight, Camera, X } from 'lucide-react';
|
||||
|
||||
interface Tank {
|
||||
name: string;
|
||||
|
|
@ -75,107 +75,77 @@ export default function ReservoirChecklist({ onComplete, onBack, isPhotoRequired
|
|||
|
||||
const status = getStatus(levelPercent);
|
||||
const statusConfig = {
|
||||
OK: { color: 'bg-success', text: 'Good', badge: 'badge-success' },
|
||||
LOW: { color: 'bg-warning', text: 'Low - Needs Refill', badge: 'badge-warning' },
|
||||
CRITICAL: { color: 'bg-destructive', text: 'Critical - Refill Now!', badge: 'badge-destructive' },
|
||||
OK: { color: 'bg-success', text: 'Good' },
|
||||
LOW: { color: 'bg-warning', text: 'Low' },
|
||||
CRITICAL: { color: 'bg-destructive', text: 'Critical' },
|
||||
}[status];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Header */}
|
||||
<div className="mb-6 md:mb-10">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="btn btn-secondary p-2"
|
||||
aria-label="Back"
|
||||
>
|
||||
<ArrowLeft size={18} />
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-tertiary rounded-md transition-colors">
|
||||
<ArrowLeft size={16} className="text-secondary" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Reservoir Checks</h1>
|
||||
<p className="text-secondary text-sm mt-0.5">
|
||||
Tank {currentTankIndex + 1} of {tanks.length}
|
||||
</p>
|
||||
<p className="text-xs text-tertiary">Reservoir {currentTankIndex + 1}/{tanks.length}</p>
|
||||
<h1 className="text-lg font-semibold text-primary">{currentTank.name}</h1>
|
||||
</div>
|
||||
<span className="text-xs text-tertiary">{currentTank.type}</span>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="h-1 bg-tertiary rounded-full overflow-hidden">
|
||||
{/* Progress */}
|
||||
<div className="flex gap-1 mb-6">
|
||||
{tanks.map((_, i) => (
|
||||
<div
|
||||
className="h-full bg-accent transition-all duration-normal"
|
||||
style={{ width: `${((currentTankIndex + 1) / tanks.length) * 100}%` }}
|
||||
key={i}
|
||||
className={`h-1 flex-1 rounded-full ${i <= currentTankIndex ? 'bg-accent' : 'bg-tertiary'}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Content - Two Column on Desktop */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
||||
{/* Left Column - Level Control */}
|
||||
<div className="card p-6 md:p-8">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-4xl mb-3">💧</div>
|
||||
<h2 className="text-xl font-semibold text-primary">{currentTank.name}</h2>
|
||||
<p className="text-sm text-tertiary">
|
||||
{currentTank.type === 'VEG' ? 'Vegetative' : 'Flowering'} Tank
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Visual Level Indicator */}
|
||||
<div className="relative h-40 md:h-48 bg-tertiary rounded-lg overflow-hidden mb-4">
|
||||
{/* Level Indicator */}
|
||||
<div className="card p-4 mb-4">
|
||||
<div className="relative h-32 bg-tertiary rounded-md overflow-hidden mb-3">
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 ${statusConfig.color} transition-all duration-normal`}
|
||||
className={`absolute bottom-0 left-0 right-0 ${statusConfig.color} transition-all duration-200`}
|
||||
style={{ height: `${levelPercent}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent" />
|
||||
</div>
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-4xl md:text-5xl font-semibold text-white drop-shadow-md">
|
||||
{levelPercent}%
|
||||
</span>
|
||||
<span className="text-3xl font-semibold text-white drop-shadow-sm">{levelPercent}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={levelPercent}
|
||||
onChange={(e) => setLevelPercent(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-tertiary rounded-full appearance-none cursor-pointer accent-accent mb-4"
|
||||
className="w-full h-1.5 bg-tertiary rounded-full appearance-none cursor-pointer accent-accent"
|
||||
/>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className={`${statusConfig.color} text-white text-center py-3 rounded-md font-medium`}>
|
||||
<div className={`mt-3 text-center text-sm font-medium ${status === 'OK' ? 'text-success' : status === 'LOW' ? 'text-warning' : 'text-destructive'
|
||||
}`}>
|
||||
{statusConfig.text}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Notes & Photo */}
|
||||
<div className="space-y-6">
|
||||
{/* Notes */}
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-2">
|
||||
Notes (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
{/* Notes - compact */}
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Any issues or observations..."
|
||||
className="input w-full resize-none"
|
||||
rows={4}
|
||||
placeholder="Notes (optional)"
|
||||
className="input w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Photo Upload */}
|
||||
{/* Photo - only show when needed */}
|
||||
{(isPhotoRequired || status !== 'OK') && (
|
||||
<div className="card p-6">
|
||||
<label className="block text-sm font-medium text-secondary mb-2">
|
||||
{isPhotoRequired ? 'Photo (Required)' : 'Photo (Recommended)'}
|
||||
</label>
|
||||
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="file"
|
||||
id="photo-upload"
|
||||
|
|
@ -188,51 +158,34 @@ export default function ReservoirChecklist({ onComplete, onBack, isPhotoRequired
|
|||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{photo ? (
|
||||
<div className="relative">
|
||||
<img src={photo} alt="Preview" className="w-full max-h-48 object-cover rounded-md" />
|
||||
<img src={photo} alt="Preview" className="w-full h-24 object-cover rounded-md" />
|
||||
<button
|
||||
onClick={() => setPhoto(null)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-destructive text-white rounded-full shadow-lg"
|
||||
className="absolute top-1 right-1 p-1 bg-black/50 rounded-full"
|
||||
>
|
||||
<X size={14} />
|
||||
<X size={12} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
htmlFor="photo-upload"
|
||||
className={`flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-md cursor-pointer transition-colors ${isPhotoRequired
|
||||
? 'border-destructive/50 bg-destructive-muted hover:border-destructive'
|
||||
: 'border-subtle hover:border-default hover:bg-tertiary'
|
||||
className={`flex items-center justify-center gap-2 p-3 border border-dashed rounded-md cursor-pointer text-sm ${isPhotoRequired ? 'border-destructive/50 text-destructive' : 'border-subtle text-tertiary'
|
||||
}`}
|
||||
>
|
||||
<Camera size={24} className="text-tertiary mb-2" />
|
||||
<span className="text-sm font-medium text-primary">Tap to capture</span>
|
||||
<span className="text-xs text-tertiary">Tank level photo</span>
|
||||
<Camera size={14} />
|
||||
{isPhotoRequired ? 'Photo required' : 'Add photo'}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="btn btn-secondary flex-1 h-12 md:h-14"
|
||||
>
|
||||
Back
|
||||
{/* Actions */}
|
||||
<button onClick={handleNext} className="btn btn-primary w-full">
|
||||
{isLastTank ? 'Complete' : 'Next'}
|
||||
{!isLastTank && <ChevronRight size={14} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="btn btn-primary flex-1 h-12 md:h-14"
|
||||
>
|
||||
{isLastTank ? 'Complete' : 'Next Tank'}
|
||||
{!isLastTank && <ChevronRight size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
IrrigationCheckData,
|
||||
PlantHealthCheckData
|
||||
} from '../lib/walkthroughApi';
|
||||
import { ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug } from 'lucide-react';
|
||||
import { ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug, ChevronRight, Clock } from 'lucide-react';
|
||||
import { useToast } from '../context/ToastContext';
|
||||
|
||||
type Step = 'start' | 'reservoir' | 'irrigation' | 'plant-health' | 'summary';
|
||||
|
|
@ -172,129 +172,80 @@ export default function DailyWalkthroughPage() {
|
|||
|
||||
// Summary Step
|
||||
if (currentStep === 'summary') {
|
||||
const totalChecks = reservoirChecks.length + irrigationChecks.length + plantHealthChecks.length;
|
||||
const issues = [
|
||||
...reservoirChecks.filter(c => c.status !== 'OK'),
|
||||
...irrigationChecks.filter(c => !c.waterFlow || !c.nutrientsMixed || c.drippersWorking < c.drippersTotal),
|
||||
...plantHealthChecks.filter(c => c.healthStatus !== 'GOOD' || c.pestsObserved)
|
||||
].length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="mb-6 md:mb-10">
|
||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Review & Submit</h1>
|
||||
<p className="text-secondary mt-1">Review your walkthrough before submitting</p>
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-3xl">
|
||||
{/* Compact header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-xl font-semibold text-primary">Review & Submit</h1>
|
||||
</div>
|
||||
|
||||
{/* Summary Grid - 1 col on mobile, 3 cols on desktop */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6 mb-6 md:mb-8">
|
||||
{/* Reservoir Summary */}
|
||||
<div className="card p-5 md:p-6">
|
||||
<h3 className="font-medium text-primary mb-4 flex items-center gap-2">
|
||||
<Droplets size={18} className="text-accent" />
|
||||
Reservoirs ({reservoirChecks.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{reservoirChecks.map((check, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-3 bg-tertiary rounded-md">
|
||||
<span className="text-sm text-primary">{check.tankName}</span>
|
||||
<span className={`
|
||||
badge text-[10px]
|
||||
${check.status === 'OK' ? 'badge-success' : check.status === 'LOW' ? 'badge-warning' : 'badge-destructive'}
|
||||
`}>
|
||||
{check.levelPercent}% — {check.status}
|
||||
</span>
|
||||
{/* Summary stats */}
|
||||
<div className="flex justify-center gap-6 mb-6">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-semibold text-primary">{totalChecks}</div>
|
||||
<div className="text-xs text-tertiary uppercase tracking-wider">Checks</div>
|
||||
</div>
|
||||
))}
|
||||
{reservoirChecks.length === 0 && (
|
||||
<p className="text-sm text-tertiary text-center py-4">No checks recorded</p>
|
||||
)}
|
||||
<div className="w-px bg-subtle" />
|
||||
<div className="text-center">
|
||||
<div className={`text-3xl font-semibold ${issues > 0 ? 'text-warning' : 'text-success'}`}>{issues}</div>
|
||||
<div className="text-xs text-tertiary uppercase tracking-wider">Issues</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Irrigation Summary */}
|
||||
<div className="card p-5 md:p-6">
|
||||
<h3 className="font-medium text-primary mb-4 flex items-center gap-2">
|
||||
<Sprout size={18} className="text-accent" />
|
||||
Irrigation ({irrigationChecks.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{irrigationChecks.map((check, i) => (
|
||||
<div key={i} className="p-3 bg-tertiary rounded-md">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-primary">{check.zoneName}</span>
|
||||
<span className="text-xs text-secondary">
|
||||
{check.drippersWorking}/{check.drippersTotal}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3 text-[10px]">
|
||||
<span className={check.waterFlow ? 'text-success' : 'text-destructive'}>
|
||||
{check.waterFlow ? '✓' : '✗'} Water
|
||||
</span>
|
||||
<span className={check.nutrientsMixed ? 'text-success' : 'text-destructive'}>
|
||||
{check.nutrientsMixed ? '✓' : '✗'} Nutrients
|
||||
</span>
|
||||
<span className={check.scheduleActive ? 'text-success' : 'text-destructive'}>
|
||||
{check.scheduleActive ? '✓' : '✗'} Schedule
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{irrigationChecks.length === 0 && (
|
||||
<p className="text-sm text-tertiary text-center py-4">No checks recorded</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plant Health Summary */}
|
||||
<div className="card p-5 md:p-6">
|
||||
<h3 className="font-medium text-primary mb-4 flex items-center gap-2">
|
||||
<Bug size={18} className="text-accent" />
|
||||
Plant Health ({plantHealthChecks.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{plantHealthChecks.map((check, i) => (
|
||||
<div key={i} className="p-3 bg-tertiary rounded-md">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm font-medium text-primary">{check.zoneName}</span>
|
||||
<span className={`
|
||||
badge text-[10px]
|
||||
${check.healthStatus === 'GOOD' ? 'badge-success' : check.healthStatus === 'FAIR' ? 'badge-warning' : 'badge-destructive'}
|
||||
`}>
|
||||
{check.healthStatus}
|
||||
</span>
|
||||
</div>
|
||||
{check.pestsObserved && (
|
||||
<div className="text-xs text-destructive mt-1">
|
||||
🐛 Pests: {check.pestType || 'Observed'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{plantHealthChecks.length === 0 && (
|
||||
<p className="text-sm text-tertiary text-center py-4">No checks recorded</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Compact summary cards */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-6">
|
||||
<SummaryCard
|
||||
icon={Droplets}
|
||||
label="Reservoirs"
|
||||
count={reservoirChecks.length}
|
||||
status={reservoirChecks.every(c => c.status === 'OK') ? 'good' : 'warning'}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Sprout}
|
||||
label="Irrigation"
|
||||
count={irrigationChecks.length}
|
||||
status={irrigationChecks.every(c => c.waterFlow && c.nutrientsMixed) ? 'good' : 'warning'}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Bug}
|
||||
label="Plant Health"
|
||||
count={plantHealthChecks.length}
|
||||
status={plantHealthChecks.every(c => c.healthStatus === 'GOOD' && !c.pestsObserved) ? 'good' : 'warning'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-destructive-muted text-destructive rounded-md text-sm flex items-center gap-2 max-w-2xl mx-auto">
|
||||
<AlertCircle size={16} />
|
||||
<div className="mb-4 p-3 bg-destructive-muted text-destructive rounded-md text-sm flex items-center gap-2">
|
||||
<AlertCircle size={14} />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 max-w-2xl mx-auto">
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setCurrentStep('plant-health')}
|
||||
disabled={isLoading}
|
||||
className="btn btn-secondary flex-1 h-12 md:h-14"
|
||||
className="btn btn-secondary px-6"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmitWalkthrough}
|
||||
disabled={isLoading}
|
||||
className="btn btn-primary flex-1 h-12 md:h-14"
|
||||
className="btn btn-primary flex-1"
|
||||
>
|
||||
{isLoading ? <Loader2 size={16} className="animate-spin" /> : <Check size={16} />}
|
||||
{isLoading ? 'Submitting...' : 'Submit Walkthrough'}
|
||||
{isLoading ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />}
|
||||
{isLoading ? 'Submitting...' : 'Complete Walkthrough'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -302,82 +253,85 @@ export default function DailyWalkthroughPage() {
|
|||
);
|
||||
}
|
||||
|
||||
// Start Screen
|
||||
const steps = [
|
||||
{ id: 'reservoir', title: 'Reservoir Checks', description: 'Check all veg and flower tank levels', icon: Droplets },
|
||||
{ id: 'irrigation', title: 'Irrigation System', description: 'Verify drippers and water flow', icon: Sprout },
|
||||
{ id: 'plant-health', title: 'Plant Health', description: 'Spot check for pests and health', icon: Bug },
|
||||
];
|
||||
// Start Screen - Refined
|
||||
const today = new Date();
|
||||
const greeting = today.getHours() < 12 ? 'Good morning' : today.getHours() < 17 ? 'Good afternoon' : 'Good evening';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8 md:mb-12 text-center">
|
||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary mb-2">Daily Walkthrough</h1>
|
||||
<p className="text-secondary">
|
||||
{new Date().toLocaleDateString('en-US', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
})}
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Minimal header */}
|
||||
<div className="text-center mb-8">
|
||||
<p className="text-sm text-tertiary mb-1">{today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}</p>
|
||||
<h1 className="text-xl font-semibold text-primary">{greeting}</h1>
|
||||
</div>
|
||||
|
||||
{/* Task list - tight and intentional */}
|
||||
<div className="card overflow-hidden">
|
||||
<div className="p-4 border-b border-subtle">
|
||||
<h2 className="text-sm font-medium text-primary">Daily Walkthrough</h2>
|
||||
<p className="text-xs text-tertiary flex items-center gap-1 mt-0.5">
|
||||
<Clock size={10} />
|
||||
~15 min
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Centered Card */}
|
||||
<div className="card p-6 md:p-10 lg:p-12 max-w-2xl mx-auto">
|
||||
<div className="text-center mb-8 md:mb-10">
|
||||
<div className="text-5xl md:text-6xl mb-4">☀️</div>
|
||||
<h2 className="text-xl md:text-2xl font-semibold text-primary mb-2">Good Morning!</h2>
|
||||
<p className="text-secondary">Ready to start your daily facility walkthrough?</p>
|
||||
</div>
|
||||
|
||||
{/* Steps Preview */}
|
||||
<div className="space-y-3 mb-8">
|
||||
{steps.map((step, index) => {
|
||||
const Icon = step.icon;
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className="flex items-center gap-4 p-4 md:p-5 bg-tertiary rounded-lg hover:bg-secondary transition-colors duration-fast"
|
||||
>
|
||||
<div className="w-10 h-10 md:w-12 md:h-12 rounded-lg bg-accent-muted flex items-center justify-center">
|
||||
<Icon size={20} className="text-accent md:w-6 md:h-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-primary">
|
||||
{index + 1}. {step.title}
|
||||
</h3>
|
||||
<p className="text-xs md:text-sm text-tertiary">{step.description}</p>
|
||||
</div>
|
||||
<div className="w-5 h-5 rounded-full border-2 border-subtle" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="divide-y divide-subtle">
|
||||
<StepRow icon={Droplets} label="Reservoir Checks" />
|
||||
<StepRow icon={Sprout} label="Irrigation System" />
|
||||
<StepRow icon={Bug} label="Plant Health" />
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-destructive-muted text-destructive rounded-md text-sm flex items-center gap-2">
|
||||
<AlertCircle size={16} />
|
||||
<div className="p-3 bg-destructive-muted text-destructive text-xs flex items-center gap-2">
|
||||
<AlertCircle size={12} />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={handleStartWalkthrough}
|
||||
disabled={isLoading}
|
||||
className="btn btn-primary w-full h-12 md:h-14 text-base"
|
||||
className="btn btn-primary w-full"
|
||||
>
|
||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : null}
|
||||
{isLoading ? 'Starting...' : 'Start Walkthrough'}
|
||||
{isLoading ? <Loader2 size={14} className="animate-spin" /> : 'Begin'}
|
||||
</button>
|
||||
|
||||
<div className="mt-6 p-4 bg-accent-muted rounded-lg border border-accent/20">
|
||||
<p className="text-sm text-accent">
|
||||
<strong>💡 Tip:</strong> This walkthrough typically takes 15–20 minutes.
|
||||
Have your device ready for photos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Compact step row
|
||||
function StepRow({ icon: Icon, label }: { icon: typeof Droplets; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<div className="w-7 h-7 rounded-md bg-tertiary flex items-center justify-center">
|
||||
<Icon size={14} className="text-secondary" />
|
||||
</div>
|
||||
<span className="text-sm text-primary flex-1">{label}</span>
|
||||
<ChevronRight size={14} className="text-tertiary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Summary card
|
||||
function SummaryCard({ icon: Icon, label, count, status }: {
|
||||
icon: typeof Droplets;
|
||||
label: string;
|
||||
count: number;
|
||||
status: 'good' | 'warning';
|
||||
}) {
|
||||
return (
|
||||
<div className="card p-4 text-center">
|
||||
<div className={`w-8 h-8 rounded-md mx-auto mb-2 flex items-center justify-center ${status === 'good' ? 'bg-success-muted text-success' : 'bg-warning-muted text-warning'
|
||||
}`}>
|
||||
<Icon size={14} />
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-primary">{count}</div>
|
||||
<div className="text-[10px] text-tertiary uppercase tracking-wider">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue