Add full PWA support (icons, manifest, service worker, pwa lib and bridge) and introduce install/offline UI. Generate app icons via a new scripts/generate-icons.js and include multiple icon sizes in public/icons; update manifest and layout head meta/preconnect. Revamp global styles (globals.css) with new utilities, animations, spacing for bottom nav, card shadows, skeletons and accessibility tweaks. Polish major screens (login, collections, map, settings) with improved headers, loading states, touch/press feedback, and layout adjustments; add new components (InstallPrompt, OfflineIndicator, PWABridge) and update various components to match new visual/UX patterns. Update Dockerfile to generate icons and use Next.js static export output (/out) for nginx, and bump package setup to support icon generation.
297 lines
No EOL
12 KiB
TypeScript
297 lines
No EOL
12 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 {
|
|
Settings,
|
|
Clock,
|
|
Crosshair,
|
|
LogOut,
|
|
Save,
|
|
RotateCcw,
|
|
Info,
|
|
ChevronRight,
|
|
Signal,
|
|
MapPin,
|
|
Shield,
|
|
} 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 });
|
|
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-gray-50">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="w-10 h-10 border-[3px] border-orange-500 border-t-transparent rounded-full animate-spin" />
|
|
<p className="text-sm text-gray-400">Loading...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isLoggedIn) return null;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 pb-nav">
|
|
{/* Header */}
|
|
<header className="bg-gray-900 text-white safe-top">
|
|
<div className="px-4 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-orange-400 to-orange-600 rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/30">
|
|
<Settings size={22} className="text-white" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h1 className="text-lg font-bold">Settings</h1>
|
|
<p className="text-xs text-gray-400 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-orange-500" />
|
|
<h2 className="text-xs font-bold text-gray-500 uppercase tracking-wider">
|
|
Location Tracking
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-2xl card-shadow overflow-hidden">
|
|
{/* Update Interval */}
|
|
<div className="p-4 border-b border-gray-50">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Clock size={16} className="text-gray-400" />
|
|
<span className="text-sm font-semibold text-gray-800">Ping Interval</span>
|
|
</div>
|
|
<span className="text-xs font-bold text-orange-600 bg-orange-50 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)}
|
|
className={cn(
|
|
'py-2.5 rounded-xl text-xs font-semibold transition-all',
|
|
'touch-manipulation press-feedback-sm',
|
|
settings.locationInterval === option.value
|
|
? 'bg-orange-500 text-white shadow-sm shadow-orange-500/25'
|
|
: 'bg-gray-50 text-gray-600 active:bg-gray-100'
|
|
)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Minimum Accuracy */}
|
|
<div className="p-4 border-b border-gray-50">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Crosshair size={16} className="text-gray-400" />
|
|
<span className="text-sm font-semibold text-gray-800">Accuracy Filter</span>
|
|
</div>
|
|
<span className="text-xs font-bold text-orange-600 bg-orange-50 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)}
|
|
className={cn(
|
|
'py-2.5 rounded-xl text-xs font-semibold transition-all flex flex-col items-center',
|
|
'touch-manimotion press-feedback-sm',
|
|
settings.minimumAccuracy === option.value
|
|
? 'bg-orange-500 text-white shadow-sm shadow-orange-500/25'
|
|
: 'bg-gray-50 text-gray-600 active:bg-gray-100'
|
|
)}
|
|
>
|
|
<span>{option.label}</span>
|
|
<span className={cn(
|
|
'text-[9px] mt-0.5',
|
|
settings.minimumAccuracy === option.value ? 'text-orange-100' : 'text-gray-400'
|
|
)}>
|
|
{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-blue-50 rounded-xl flex items-center justify-center">
|
|
<MapPin size={16} className="text-blue-500" />
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-semibold text-gray-800 block">High Accuracy GPS</span>
|
|
<span className="text-[11px] text-gray-400">Uses more battery for precision</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => updateSetting('enableHighAccuracy', !settings.enableHighAccuracy)}
|
|
className={cn(
|
|
'w-[52px] h-[30px] rounded-full transition-colors relative',
|
|
settings.enableHighAccuracy ? 'bg-orange-500' : 'bg-gray-200'
|
|
)}
|
|
>
|
|
<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-gray-50">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-green-50 rounded-xl flex items-center justify-center">
|
|
<Shield size={16} className="text-green-500" />
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-semibold text-gray-800 block">Keep Screen On</span>
|
|
<span className="text-[11px] text-gray-400">Prevents phone from sleeping</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => updateSetting('offlineMode', !settings.offlineMode)}
|
|
className={cn(
|
|
'w-[52px] h-[30px] rounded-full transition-colors relative',
|
|
settings.offlineMode ? 'bg-orange-500' : 'bg-gray-200'
|
|
)}
|
|
>
|
|
<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>
|
|
|
|
{/* About */}
|
|
<section>
|
|
<div className="flex items-center gap-2 mb-3 px-1">
|
|
<Info size={14} className="text-orange-500" />
|
|
<h2 className="text-xs font-bold text-gray-500 uppercase tracking-wider">About</h2>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-2xl card-shadow overflow-hidden">
|
|
<div className="p-5 text-center">
|
|
<div className="w-14 h-14 bg-gradient-to-br from-orange-400 to-orange-600 rounded-2xl mx-auto mb-3 flex items-center justify-center shadow-lg shadow-orange-500/20">
|
|
<span className="text-white font-bold text-xl">1D</span>
|
|
</div>
|
|
<h3 className="text-lg font-bold text-gray-900">1Driver</h3>
|
|
<p className="text-xs text-gray-400 mt-1">Version 1.0.0</p>
|
|
<p className="text-[11px] text-gray-300 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-white border border-gray-200 text-gray-700 active:bg-gray-50',
|
|
'touch-manipulation press-feedback flex items-center justify-center gap-2 card-shadow'
|
|
)}
|
|
>
|
|
<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-red-500 active:bg-red-600 transition-colors shadow-lg shadow-red-500/20',
|
|
'touch-manimotion press-feedback flex items-center justify-center gap-2'
|
|
)}
|
|
>
|
|
<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 shadow-lg text-sm font-medium text-white text-center bg-green-500 flex items-center justify-center gap-2 card-shadow-lg">
|
|
<Save size={16} />
|
|
Settings saved
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<BottomNav />
|
|
</div>
|
|
);
|
|
} |