WFDASHDRIVER-APP/app/settings/page.tsx
SJCoetz 3c8fc91b3e feat: add complete dark mode support with settings toggle
- Set up class-based dark mode in Tailwind config
- Add pre-hydration dark mode application to avoid theme flash on initial load
- Implement accessible dark mode toggle in settings page
- Update all UI surfaces (headers, bottom nav, maps) with dark mode color schemes
- Add comprehensive global CSS overrides for arbitrary Tailwind classes
- Add brand accent strips to header and bottom nav for consistent visual framing
- Refine map waypoint styling: reduce dot size, update label positioning and styling
2026-07-07 18:28:19 +02:00

363 lines
No EOL
15 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/components/AuthProvider';
import BottomNav from '@/components/BottomNav';
import { getSettings, saveSettings, logout } from '@/lib/pocketbase';
import { AppSettings, DEFAULT_SETTINGS } from '@/lib/types';
import { cn } from '@/lib/utils';
import { setDarkMode, applyDarkMode } from '@/lib/theme';
import {
Settings,
Clock,
Crosshair,
LogOut,
Save,
RotateCcw,
Info,
ChevronRight,
Signal,
MapPin,
Shield,
Moon,
Sun,
} from 'lucide-react';
const INTERVAL_OPTIONS = [
{ value: 30, label: '30s' },
{ value: 60, label: '1 min' },
{ value: 120, label: '2 min' },
{ value: 300, label: '5 min' },
{ value: 600, label: '10 min' },
];
const ACCURACY_OPTIONS = [
{ value: 10, label: '10m', desc: 'High precision' },
{ value: 50, label: '50m', desc: 'Balanced' },
{ value: 100, label: '100m', desc: 'Battery saver' },
{ value: 500, label: '500m', desc: 'Low power' },
];
export default function SettingsPage() {
const router = useRouter();
const { isLoggedIn, user, isLoading: authLoading } = useAuth();
const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
const [saved, setSaved] = useState(false);
useEffect(() => {
if (!authLoading && !isLoggedIn) router.push('/login/');
}, [isLoggedIn, authLoading, router]);
useEffect(() => {
setSettings(getSettings());
}, []);
const updateSetting = <K extends keyof AppSettings>(key: K, value: AppSettings[K]) => {
const updated = { ...settings, [key]: value };
setSettings(updated);
saveSettings({ [key]: value });
// Dark mode also needs to flip the <html> class — the rest of
// AppSettings is purely persisted, but darkMode drives a class
// Tailwind keys off of, so we apply it inline here.
if (key === 'darkMode') setDarkMode(Boolean(value));
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
const resetSettings = () => {
if (confirm('Reset all settings to defaults?')) {
setSettings(DEFAULT_SETTINGS);
saveSettings(DEFAULT_SETTINGS);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}
};
if (authLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#f2f1ed]">
<div className="flex flex-col items-center gap-3">
<div className="w-10 h-10 border-[3px] border-[#f54e00] border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-[rgba(38,37,30,0.55)]">Loading...</p>
</div>
</div>
);
}
if (!isLoggedIn) return null;
return (
<div className="min-h-screen bg-[#f2f1ed] pb-nav">
{/* Header — Cursor cream surface, dark ink icon tile */}
<header className="bg-[#e6e5e0] text-[#26251e] safe-top border-b border-[rgba(38,37,30,0.1)]">
<div className="px-4 py-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[#26251e] rounded-2xl flex items-center justify-center" style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.15)' }}>
<Settings size={22} className="text-white" />
</div>
<div className="flex-1 min-w-0">
<h1 className="text-lg font-bold text-[#26251e]">Settings</h1>
<p className="text-xs text-[rgba(38,37,30,0.55)] truncate">{user?.name || user?.email}</p>
</div>
</div>
</div>
</header>
<main className="p-4 space-y-6">
{/* Location Settings */}
<section>
<div className="flex items-center gap-2 mb-3 px-1">
<Signal size={14} className="text-[#f54e00]" />
<h2 className="text-xs font-bold text-[rgba(38,37,30,0.55)] uppercase tracking-wider">
Location Tracking
</h2>
</div>
<div className="bg-[#e6e5e0] rounded-2xl overflow-hidden border border-[rgba(38,37,30,0.1)]">
{/* Update Interval */}
<div className="p-4 border-b border-[rgba(38,37,30,0.08)]">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Clock size={16} className="text-[rgba(38,37,30,0.55)]" />
<span className="text-sm font-semibold text-[#26251e]">Ping Interval</span>
</div>
<span className="text-xs font-bold text-[#f54e00] bg-[#ebeae5] px-2.5 py-1 rounded-lg">
{settings.locationInterval >= 60 ? `${settings.locationInterval / 60} min` : `${settings.locationInterval}s`}
</span>
</div>
<div className="grid grid-cols-5 gap-1.5">
{INTERVAL_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => updateSetting('locationInterval', option.value)}
aria-pressed={settings.locationInterval === option.value}
className={cn(
'py-2.5 rounded-xl text-xs font-semibold transition-all',
'touch-manipulation press-feedback-sm',
settings.locationInterval === option.value
? 'bg-[#26251e] text-white'
: 'bg-[#ebeae5] text-[rgba(38,37,30,0.7)] active:bg-[#e1e0db]'
)}
>
{option.label}
</button>
))}
</div>
</div>
{/* Minimum Accuracy */}
<div className="p-4 border-b border-[rgba(38,37,30,0.08)]">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Crosshair size={16} className="text-[rgba(38,37,30,0.55)]" />
<span className="text-sm font-semibold text-[#26251e]">Accuracy Filter</span>
</div>
<span className="text-xs font-bold text-[#f54e00] bg-[#ebeae5] px-2.5 py-1 rounded-lg">
{settings.minimumAccuracy}m
</span>
</div>
<div className="grid grid-cols-4 gap-1.5">
{ACCURACY_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => updateSetting('minimumAccuracy', option.value)}
aria-pressed={settings.minimumAccuracy === option.value}
className={cn(
'py-2.5 rounded-xl text-xs font-semibold transition-all flex flex-col items-center',
'touch-manipulation press-feedback-sm',
settings.minimumAccuracy === option.value
? 'bg-[#26251e] text-white'
: 'bg-[#ebeae5] text-[rgba(38,37,30,0.7)] active:bg-[#e1e0db]'
)}
>
<span>{option.label}</span>
<span className={cn(
'text-[9px] mt-0.5',
settings.minimumAccuracy === option.value ? 'text-[rgba(242,241,237,0.7)]' : 'text-[rgba(38,37,30,0.55)]'
)}>
{option.desc}
</span>
</button>
))}
</div>
</div>
{/* High Accuracy Toggle */}
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-[#ebeae5] rounded-xl flex items-center justify-center">
<MapPin size={16} className="text-[#26251e]" />
</div>
<div>
<span className="text-sm font-semibold text-[#26251e] block">High Accuracy GPS</span>
<span className="text-[11px] text-[rgba(38,37,30,0.55)]">Uses more battery for precision</span>
</div>
</div>
<button
onClick={() => updateSetting('enableHighAccuracy', !settings.enableHighAccuracy)}
role="switch"
aria-checked={settings.enableHighAccuracy}
aria-label="High Accuracy GPS"
className={cn(
'w-[52px] h-[30px] rounded-full transition-colors relative',
settings.enableHighAccuracy ? 'bg-[#f54e00]' : 'bg-[rgba(38,37,30,0.15)]'
)}
>
<div className={cn(
'absolute top-[3px] w-6 h-6 bg-white rounded-full shadow-md transition-transform',
settings.enableHighAccuracy ? 'translate-x-[26px]' : 'translate-x-[3px]'
)} />
</button>
</div>
</div>
{/* Screen Wake Lock */}
<div className="p-4 border-t border-[rgba(38,37,30,0.08)]">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-[#e6f1ec] rounded-xl flex items-center justify-center">
<Shield size={16} className="text-[#125a3e]" />
</div>
<div>
<span className="text-sm font-semibold text-[#26251e] block">Keep Screen On</span>
<span className="text-[11px] text-[rgba(38,37,30,0.55)]">Prevents phone from sleeping</span>
</div>
</div>
<button
onClick={() => updateSetting('offlineMode', !settings.offlineMode)}
role="switch"
aria-checked={settings.offlineMode}
aria-label="Keep screen on"
className={cn(
'w-[52px] h-[30px] rounded-full transition-colors relative',
settings.offlineMode ? 'bg-[#f54e00]' : 'bg-[rgba(38,37,30,0.15)]'
)}
>
<div className={cn(
'absolute top-[3px] w-6 h-6 bg-white rounded-full shadow-md transition-transform',
settings.offlineMode ? 'translate-x-[26px]' : 'translate-x-[3px]'
)} />
</button>
</div>
</div>
</div>
</section>
{/* Appearance */}
<section>
<div className="flex items-center gap-2 mb-3 px-1">
{settings.darkMode
? <Moon size={14} className="text-[#f54e00]" />
: <Sun size={14} className="text-[#f54e00]" />}
<h2 className="text-xs font-bold text-[rgba(38,37,30,0.55)] uppercase tracking-wider">
Appearance
</h2>
</div>
<div className="bg-[#e6e5e0] rounded-2xl overflow-hidden border border-[rgba(38,37,30,0.1)]">
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-[#ebeae5] rounded-xl flex items-center justify-center">
{settings.darkMode
? <Moon size={16} className="text-[#26251e]" />
: <Sun size={16} className="text-[#f54e00]" />}
</div>
<div>
<span className="text-sm font-semibold text-[#26251e] block">Dark Mode</span>
<span className="text-[11px] text-[rgba(38,37,30,0.55)]">Easier on the eyes at night</span>
</div>
</div>
<button
onClick={() => updateSetting('darkMode', !settings.darkMode)}
role="switch"
aria-checked={settings.darkMode}
aria-label="Dark mode"
className={cn(
'w-[52px] h-[30px] rounded-full transition-colors relative',
settings.darkMode ? 'bg-[#f54e00]' : 'bg-[rgba(38,37,30,0.15)]'
)}
>
<div className={cn(
'absolute top-[3px] w-6 h-6 bg-white rounded-full shadow-md transition-transform',
settings.darkMode ? 'translate-x-[26px]' : 'translate-x-[3px]'
)} />
</button>
</div>
</div>
</div>
</section>
{/* About */}
<section>
<div className="flex items-center gap-2 mb-3 px-1">
<Info size={14} className="text-[#f54e00]" />
<h2 className="text-xs font-bold text-[rgba(38,37,30,0.55)] uppercase tracking-wider">About</h2>
</div>
<div className="bg-[#e6e5e0] rounded-2xl overflow-hidden border border-[rgba(38,37,30,0.1)]">
<div className="p-5 text-center">
<div className="w-14 h-14 bg-[#26251e] rounded-2xl mx-auto mb-3 flex items-center justify-center" style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.15)' }}>
<span className="text-white font-bold text-xl">1D</span>
</div>
<h3 className="text-lg font-bold text-[#26251e]">1Driver</h3>
<p className="text-xs text-[rgba(38,37,30,0.55)] mt-1">Version 1.0.0</p>
<p className="text-[11px] text-[rgba(38,37,30,0.4)] mt-0.5">Collection Management App</p>
</div>
</div>
</section>
{/* Actions */}
<div className="space-y-2.5">
<button
onClick={resetSettings}
className={cn(
'w-full py-3.5 px-4 rounded-2xl text-sm font-semibold',
'bg-[#e6e5e0] border border-[rgba(38,37,30,0.1)] text-[#26251e] active:bg-[#ebeae5]',
'touch-manipulation press-feedback flex items-center justify-center gap-2'
)}
>
<RotateCcw size={16} />
Reset to Defaults
</button>
<button
onClick={() => {
if (confirm('Are you sure you want to sign out?')) {
logout();
}
}}
className={cn(
'w-full py-3.5 px-4 rounded-2xl text-white font-semibold text-[15px]',
'bg-[#cf2d56] active:bg-[#8a1b3a] transition-colors',
'touch-manipulation press-feedback flex items-center justify-center gap-2'
)}
style={{ boxShadow: '0 4px 12px rgba(207,45,86,0.3)' }}
>
<LogOut size={18} />
Sign Out
</button>
</div>
</main>
{/* Saved Toast */}
{saved && (
<div className="fixed top-4 left-4 right-4 z-[70] animate-slide-down safe-top">
<div
className="px-4 py-3 rounded-2xl text-sm font-medium text-white text-center bg-[#1f8a65] flex items-center justify-center gap-2"
role="status"
aria-live="polite"
style={{ boxShadow: '0 14px 32px rgba(0,0,0,0.18)' }}
>
<Save size={16} />
Settings saved
</div>
</div>
)}
<BottomNav />
</div>
);
}