fix: walkthrough pages respect theme + mobile UX improvements
- 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:
parent
77e7382504
commit
8dbcd51246
2 changed files with 364 additions and 574 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue