fix: walkthrough pages respect theme + mobile UX improvements
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- Use CSS variables (bg-primary, text-primary, etc.) instead of hardcoded colors
- Both pages now properly follow light/dark theme preference
- Larger touch targets (44px+) for tablet/phone use
- Simplified controls and cleaner visual hierarchy
- Toggle checkboxes replaced with large tap-friendly buttons
- Slider with step=5 for easier touch adjustment
This commit is contained in:
fullsizemalt 2025-12-19 21:13:26 -08:00
parent 77e7382504
commit 8dbcd51246
2 changed files with 364 additions and 574 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Save, CheckSquare, Settings, Camera, Loader2, ArrowLeft, BookOpen, FileText, Droplets, Sprout, Bug } from 'lucide-react'; import { Save, CheckSquare, Camera, Loader2, ArrowLeft, Droplets, Sprout, Bug } from 'lucide-react';
import { settingsApi, WalkthroughSettings, PhotoRequirement } from '../lib/settingsApi'; import { settingsApi, WalkthroughSettings, PhotoRequirement } from '../lib/settingsApi';
import { documentsApi, Document } from '../lib/documentsApi';
import { Card } from '../components/ui/card';
import { useToast } from '../context/ToastContext'; import { useToast } from '../context/ToastContext';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { motion } from 'framer-motion';
const PHOTO_OPTIONS: { label: string; value: PhotoRequirement }[] = [ const PHOTO_OPTIONS: { label: string; value: PhotoRequirement }[] = [
{ label: 'Always Required', value: 'REQUIRED' }, { label: 'Always Required', value: 'REQUIRED' },
@ -20,7 +17,6 @@ export default function WalkthroughSettingsPage() {
const [settings, setSettings] = useState<WalkthroughSettings | null>(null); const [settings, setSettings] = useState<WalkthroughSettings | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [checklists, setChecklists] = useState<Document[]>([]);
useEffect(() => { useEffect(() => {
loadSettings(); loadSettings();
@ -29,12 +25,8 @@ export default function WalkthroughSettingsPage() {
const loadSettings = async () => { const loadSettings = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const [data, docs] = await Promise.all([ const data = await settingsApi.getWalkthrough();
settingsApi.getWalkthrough(),
documentsApi.getDocuments({ type: 'CHECKLIST', status: 'APPROVED' }).catch(() => [])
]);
setSettings(data); setSettings(data);
setChecklists(docs);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
addToast('Failed to load settings', 'error'); addToast('Failed to load settings', 'error');
@ -61,73 +53,27 @@ export default function WalkthroughSettingsPage() {
if (isLoading || !settings) { if (isLoading || !settings) {
return ( return (
<div className="min-h-screen bg-[#0B0E14] flex items-center justify-center"> <div className="min-h-screen bg-primary flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<Loader2 className="animate-spin text-emerald-500 mx-auto mb-4" size={32} /> <Loader2 className="animate-spin text-accent mx-auto mb-4" size={32} />
<p className="text-slate-400 text-sm">Loading settings...</p> <p className="text-tertiary text-sm">Loading settings...</p>
</div> </div>
</div> </div>
); );
} }
// Toggle switch component
const Toggle = ({ checked, onChange, label, description, icon: Icon }: {
checked: boolean;
onChange: () => void;
label: string;
description?: string;
icon?: typeof Droplets;
}) => (
<label className="flex items-center justify-between p-4 rounded-xl hover:bg-slate-800/30 transition-colors cursor-pointer">
<div className="flex items-center gap-4">
{Icon && (
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center",
checked ? "bg-emerald-500/20 border border-emerald-500/30" : "bg-slate-800 border border-slate-700"
)}>
<Icon size={18} className={checked ? "text-emerald-500" : "text-slate-500"} />
</div>
)}
<div>
<span className="text-sm font-bold text-white">{label}</span>
{description && <p className="text-xs text-slate-500 mt-0.5">{description}</p>}
</div>
</div>
<button
type="button"
role="switch"
aria-checked={checked}
onClick={onChange}
className={cn(
"relative w-12 h-7 rounded-full transition-colors duration-200",
checked ? 'bg-emerald-600' : 'bg-slate-700'
)}
>
<motion.span
animate={{ x: checked ? 22 : 2 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className="absolute top-1 w-5 h-5 bg-white rounded-full shadow-lg"
/>
</button>
</label>
);
return ( return (
<div className="min-h-screen bg-[#0B0E14] pb-20"> <div className="min-h-screen bg-primary pb-20">
{/* Header */} {/* Header */}
<div className="sticky top-0 z-20 bg-[#0B0E14]/95 backdrop-blur-xl border-b border-slate-800/50"> <div className="sticky top-0 z-20 bg-elevated border-b border-subtle">
<div className="max-w-2xl mx-auto px-4 py-4"> <div className="max-w-2xl mx-auto px-4 py-3">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3">
<Link to="/settings" className="p-2 rounded-lg hover:bg-slate-800 transition-colors text-slate-400"> <Link to="/settings" className="p-2 -ml-2 rounded-lg hover:bg-tertiary text-tertiary">
<ArrowLeft size={20} /> <ArrowLeft size={20} />
</Link> </Link>
<div> <div>
<h1 className="text-lg font-bold text-white uppercase italic tracking-wide"> <h1 className="text-base font-semibold text-primary">Walkthrough Settings</h1>
Walkthrough Config <p className="text-xs text-tertiary">Configure daily checklist</p>
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Configure daily checklist requirements
</p>
</div> </div>
</div> </div>
</div> </div>
@ -136,61 +82,51 @@ export default function WalkthroughSettingsPage() {
<div className="max-w-2xl mx-auto px-4 py-6"> <div className="max-w-2xl mx-auto px-4 py-6">
<form onSubmit={handleUpdate} className="space-y-6"> <form onSubmit={handleUpdate} className="space-y-6">
{/* Enabled Modules */} {/* Enabled Modules */}
<Card className="bg-[#13171F] border-slate-800 overflow-hidden"> <div className="card">
<div className="p-4 border-b border-slate-800 flex items-center gap-3"> <div className="p-4 border-b border-subtle flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-emerald-500/20 flex items-center justify-center"> <CheckSquare size={16} className="text-accent" />
<CheckSquare size={16} className="text-emerald-500" /> <span className="text-sm font-medium text-primary">Enabled Modules</span>
</div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wide">Enabled Modules</h3>
<p className="text-xs text-slate-500">Choose which checks to include</p>
</div>
</div> </div>
<div className="divide-y divide-slate-800/50"> <div className="divide-y divide-subtle">
<Toggle <Toggle
checked={settings.enableReservoirs} checked={settings.enableReservoirs}
onChange={() => setSettings({ ...settings, enableReservoirs: !settings.enableReservoirs })} onChange={() => setSettings({ ...settings, enableReservoirs: !settings.enableReservoirs })}
label="Reservoir Checks" label="Reservoir Checks"
description="Tank levels and nutrient monitoring" description="Tank levels and nutrients"
icon={Droplets} icon={Droplets}
/> />
<Toggle <Toggle
checked={settings.enableIrrigation} checked={settings.enableIrrigation}
onChange={() => setSettings({ ...settings, enableIrrigation: !settings.enableIrrigation })} onChange={() => setSettings({ ...settings, enableIrrigation: !settings.enableIrrigation })}
label="Irrigation Checks" label="Irrigation Checks"
description="Dripper counts and flow verification" description="Dripper counts and flow"
icon={Sprout} icon={Sprout}
/> />
<Toggle <Toggle
checked={settings.enablePlantHealth} checked={settings.enablePlantHealth}
onChange={() => setSettings({ ...settings, enablePlantHealth: !settings.enablePlantHealth })} onChange={() => setSettings({ ...settings, enablePlantHealth: !settings.enablePlantHealth })}
label="Plant Health Checks" label="Plant Health Checks"
description="Visual inspections and pest monitoring" description="Visual inspections"
icon={Bug} icon={Bug}
/> />
</div> </div>
</Card> </div>
{/* Photo Requirements */} {/* Photo Requirements */}
<Card className="bg-[#13171F] border-slate-800 overflow-hidden"> <div className="card">
<div className="p-4 border-b border-slate-800 flex items-center gap-3"> <div className="p-4 border-b border-subtle flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center"> <Camera size={16} className="text-accent" />
<Camera size={16} className="text-blue-500" /> <span className="text-sm font-medium text-primary">Photo Requirements</span>
</div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wide">Photo Requirements</h3>
<p className="text-xs text-slate-500">When to require photo documentation</p>
</div>
</div> </div>
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div> <div>
<label className="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-2"> <label className="block text-xs font-medium text-tertiary uppercase tracking-wider mb-2">
Reservoirs Reservoirs
</label> </label>
<select <select
value={settings.reservoirPhotos} value={settings.reservoirPhotos}
onChange={e => setSettings({ ...settings, reservoirPhotos: e.target.value as PhotoRequirement })} onChange={e => setSettings({ ...settings, reservoirPhotos: e.target.value as PhotoRequirement })}
className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-sm text-white focus:border-emerald-500/50 focus:outline-none" className="input w-full py-3"
> >
{PHOTO_OPTIONS.map(opt => ( {PHOTO_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
@ -198,13 +134,13 @@ export default function WalkthroughSettingsPage() {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-2"> <label className="block text-xs font-medium text-tertiary uppercase tracking-wider mb-2">
Irrigation Irrigation
</label> </label>
<select <select
value={settings.irrigationPhotos} value={settings.irrigationPhotos}
onChange={e => setSettings({ ...settings, irrigationPhotos: e.target.value as PhotoRequirement })} onChange={e => setSettings({ ...settings, irrigationPhotos: e.target.value as PhotoRequirement })}
className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-sm text-white focus:border-emerald-500/50 focus:outline-none" className="input w-full py-3"
> >
{PHOTO_OPTIONS.map(opt => ( {PHOTO_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
@ -212,13 +148,13 @@ export default function WalkthroughSettingsPage() {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-2"> <label className="block text-xs font-medium text-tertiary uppercase tracking-wider mb-2">
Plant Health Plant Health
</label> </label>
<select <select
value={settings.plantHealthPhotos} value={settings.plantHealthPhotos}
onChange={e => setSettings({ ...settings, plantHealthPhotos: e.target.value as PhotoRequirement })} onChange={e => setSettings({ ...settings, plantHealthPhotos: e.target.value as PhotoRequirement })}
className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-sm text-white focus:border-emerald-500/50 focus:outline-none" className="input w-full py-3"
> >
{PHOTO_OPTIONS.map(opt => ( {PHOTO_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
@ -226,55 +162,17 @@ export default function WalkthroughSettingsPage() {
</select> </select>
</div> </div>
</div> </div>
</Card> </div>
{/* Linked SOPs */}
{checklists.length > 0 && (
<Card className="bg-[#13171F] border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
<BookOpen size={16} className="text-purple-500" />
</div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wide">Linked SOPs</h3>
<p className="text-xs text-slate-500">Related checklist documents</p>
</div>
</div>
<div className="p-4 space-y-2">
{checklists.map(doc => (
<div key={doc.id} className="flex items-center gap-3 p-3 bg-slate-800/50 rounded-xl">
<FileText size={16} className="text-slate-400" />
<div className="flex-1">
<span className="text-sm text-white">{doc.title}</span>
<span className="text-xs text-slate-500 ml-2">v{doc.version}</span>
</div>
<Link
to={`/documents?id=${doc.id}`}
className="text-xs text-emerald-500 font-bold uppercase tracking-wider hover:text-emerald-400"
>
View
</Link>
</div>
))}
</div>
</Card>
)}
<button <button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-4 rounded-xl font-bold uppercase tracking-widest text-sm transition-all shadow-xl shadow-emerald-500/20 disabled:opacity-50 flex items-center justify-center gap-2" className="btn btn-primary w-full py-4 text-base"
> >
{isSaving ? ( {isSaving ? (
<> <><Loader2 size={18} className="animate-spin" /> Saving...</>
<Loader2 size={16} className="animate-spin" />
Saving...
</>
) : ( ) : (
<> <><Save size={18} /> Save Settings</>
<Save size={16} />
Save Configuration
</>
)} )}
</button> </button>
</form> </form>
@ -282,3 +180,48 @@ export default function WalkthroughSettingsPage() {
</div> </div>
); );
} }
// Toggle component
function Toggle({ checked, onChange, label, description, icon: Icon }: {
checked: boolean;
onChange: () => void;
label: string;
description?: string;
icon?: typeof Droplets;
}) {
return (
<label className="flex items-center justify-between p-4 hover:bg-tertiary transition-colors cursor-pointer active:bg-secondary">
<div className="flex items-center gap-3">
{Icon && (
<div className={cn(
"w-10 h-10 rounded-lg flex items-center justify-center",
checked ? "bg-accent-muted text-accent" : "bg-tertiary text-tertiary"
)}>
<Icon size={18} />
</div>
)}
<div>
<span className="text-sm font-medium text-primary">{label}</span>
{description && <p className="text-xs text-tertiary">{description}</p>}
</div>
</div>
<button
type="button"
role="switch"
aria-checked={checked}
onClick={onChange}
className={cn(
"relative w-12 h-7 rounded-full transition-colors",
checked ? 'bg-accent' : 'bg-tertiary'
)}
>
<span
className={cn(
"absolute top-1 w-5 h-5 bg-white rounded-full shadow transition-transform",
checked ? 'translate-x-6' : 'translate-x-1'
)}
/>
</button>
</label>
);
}