- Added package.json with dependencies for Next.js, React, and Tailwind CSS. - Created postcss.config.js for Tailwind CSS and autoprefixer integration. - Added SVG icon for the application in public/icons/icon.svg. - Created manifest.json for PWA support with app details and icon. - Configured Tailwind CSS in tailwind.config.js with custom colors and animations. - Set up TypeScript configuration in tsconfig.json for strict type checking and module resolution.
362 lines
13 KiB
TypeScript
362 lines
13 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,
|
|
Moon,
|
|
Type,
|
|
WifiOff,
|
|
LogOut,
|
|
ChevronRight,
|
|
Save,
|
|
RotateCcw,
|
|
Info,
|
|
} from 'lucide-react';
|
|
|
|
const INTERVAL_OPTIONS = [
|
|
{ value: 30, label: '30 seconds' },
|
|
{ value: 60, label: '1 minute' },
|
|
{ value: 120, label: '2 minutes' },
|
|
{ value: 300, label: '5 minutes' },
|
|
{ value: 600, label: '10 minutes' },
|
|
];
|
|
|
|
const ACCURACY_OPTIONS = [
|
|
{ value: 10, label: '10m (High)' },
|
|
{ value: 50, label: '50m (Balanced)' },
|
|
{ value: 100, label: '100m (Battery)' },
|
|
{ value: 500, label: '500m (Low)' },
|
|
];
|
|
|
|
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="w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isLoggedIn) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 pb-20">
|
|
{/* 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-10 h-10 bg-orange-500 rounded-xl flex items-center justify-center">
|
|
<Settings size={20} className="text-white" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-lg font-bold">Settings</h1>
|
|
<p className="text-xs text-gray-400">
|
|
{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">
|
|
<Crosshair size={16} className="text-orange-500" />
|
|
<h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">
|
|
Location Tracking
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl card-shadow overflow-hidden">
|
|
{/* Update Interval */}
|
|
<div className="p-4 border-b border-gray-100">
|
|
<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-medium text-gray-700">
|
|
Update Interval
|
|
</span>
|
|
</div>
|
|
<span className="text-xs text-orange-600 font-medium bg-orange-50 px-2 py-1 rounded-full">
|
|
{settings.locationInterval}s
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{INTERVAL_OPTIONS.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
onClick={() => updateSetting('locationInterval', option.value)}
|
|
className={cn(
|
|
'py-2 px-2 rounded-lg text-xs font-medium transition-all',
|
|
'touch-manipulation',
|
|
settings.locationInterval === option.value
|
|
? 'bg-orange-500 text-white shadow-sm'
|
|
: 'bg-gray-50 text-gray-600 hover:bg-gray-100'
|
|
)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="text-[10px] text-gray-400 mt-2">
|
|
How often your location is sent to the server
|
|
</p>
|
|
</div>
|
|
|
|
{/* Minimum Accuracy */}
|
|
<div className="p-4 border-b border-gray-100">
|
|
<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-medium text-gray-700">
|
|
Minimum Accuracy
|
|
</span>
|
|
</div>
|
|
<span className="text-xs text-orange-600 font-medium bg-orange-50 px-2 py-1 rounded-full">
|
|
{settings.minimumAccuracy}m
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{ACCURACY_OPTIONS.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
onClick={() => updateSetting('minimumAccuracy', option.value)}
|
|
className={cn(
|
|
'py-2 px-2 rounded-lg text-xs font-medium transition-all',
|
|
'touch-manipulation',
|
|
settings.minimumAccuracy === option.value
|
|
? 'bg-orange-500 text-white shadow-sm'
|
|
: 'bg-gray-50 text-gray-600 hover:bg-gray-100'
|
|
)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="text-[10px] text-gray-400 mt-2">
|
|
Skip location updates with worse accuracy
|
|
</p>
|
|
</div>
|
|
|
|
{/* High Accuracy Toggle */}
|
|
<div className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Crosshair size={16} className="text-gray-400" />
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-700 block">
|
|
High Accuracy Mode
|
|
</span>
|
|
<span className="text-[10px] text-gray-400">
|
|
Uses GPS for better precision
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() =>
|
|
updateSetting('enableHighAccuracy', !settings.enableHighAccuracy)
|
|
}
|
|
className={cn(
|
|
'w-12 h-7 rounded-full transition-colors relative',
|
|
settings.enableHighAccuracy ? 'bg-orange-500' : 'bg-gray-300'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'absolute top-0.5 w-6 h-6 bg-white rounded-full shadow-md transition-transform',
|
|
settings.enableHighAccuracy ? 'translate-x-5' : 'translate-x-0.5'
|
|
)}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Display Settings */}
|
|
<section>
|
|
<div className="flex items-center gap-2 mb-3 px-1">
|
|
<Type size={16} className="text-orange-500" />
|
|
<h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">
|
|
Display
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl card-shadow overflow-hidden">
|
|
{/* Font Size */}
|
|
<div className="p-4 border-b border-gray-100">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Type size={16} className="text-gray-400" />
|
|
<span className="text-sm font-medium text-gray-700">Font Size</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{(['small', 'normal', 'large'] as const).map((size) => (
|
|
<button
|
|
key={size}
|
|
onClick={() => updateSetting('fontSize', size)}
|
|
className={cn(
|
|
'flex-1 py-2.5 rounded-lg text-sm font-medium transition-all',
|
|
'touch-manipulation capitalize',
|
|
settings.fontSize === size
|
|
? 'bg-orange-500 text-white shadow-sm'
|
|
: 'bg-gray-50 text-gray-600 hover:bg-gray-100'
|
|
)}
|
|
>
|
|
{size === 'small' && (
|
|
<span className="text-xs">A</span>
|
|
)}
|
|
{size === 'normal' && (
|
|
<span className="text-sm">A</span>
|
|
)}
|
|
{size === 'large' && (
|
|
<span className="text-lg">A</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Offline Mode */}
|
|
<div className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<WifiOff size={16} className="text-gray-400" />
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-700 block">
|
|
Offline Queue
|
|
</span>
|
|
<span className="text-[10px] text-gray-400">
|
|
Queue actions when offline
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() =>
|
|
updateSetting('offlineMode', !settings.offlineMode)
|
|
}
|
|
className={cn(
|
|
'w-12 h-7 rounded-full transition-colors relative',
|
|
settings.offlineMode ? 'bg-orange-500' : 'bg-gray-300'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'absolute top-0.5 w-6 h-6 bg-white rounded-full shadow-md transition-transform',
|
|
settings.offlineMode ? 'translate-x-5' : 'translate-x-0.5'
|
|
)}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* About */}
|
|
<section>
|
|
<div className="flex items-center gap-2 mb-3 px-1">
|
|
<Info size={16} className="text-orange-500" />
|
|
<h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">
|
|
About
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl card-shadow overflow-hidden">
|
|
<div className="p-4 text-center">
|
|
<h3 className="text-lg font-bold text-gray-800">1Driver</h3>
|
|
<p className="text-xs text-gray-400 mt-1">Version 1.0.0</p>
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
Collections Management App
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Actions */}
|
|
<div className="space-y-3">
|
|
<button
|
|
onClick={resetSettings}
|
|
className={cn(
|
|
'w-full py-3 px-4 rounded-xl text-sm font-medium',
|
|
'bg-gray-200 text-gray-700 active:bg-gray-300',
|
|
'touch-manipulation 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-xl text-white font-semibold text-base',
|
|
'bg-red-500 active:bg-red-600 transition-colors',
|
|
'touch-manipulation 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-[60] animate-fade-in">
|
|
<div className="px-4 py-3 rounded-xl shadow-lg text-sm font-medium text-white text-center bg-green-500 flex items-center justify-center gap-2">
|
|
<Save size={16} />
|
|
Settings saved
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<BottomNav />
|
|
</div>
|
|
);
|
|
}
|