From db10eb95c7173b78f86feccb727fa0ce4474410d Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Wed, 27 May 2026 00:34:50 +0200 Subject: [PATCH] Add PWA support and UI redesign 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. --- .gitignore | 1 + Dockerfile | 17 +- app/globals.css | 241 ++++++++++-- app/layout.tsx | 24 +- app/login/page.tsx | 142 +++---- app/map/page.tsx | 67 ++-- app/page.tsx | 183 +++++---- app/settings/page.tsx | 261 +++++-------- components/BottomNav.tsx | 33 +- components/CollectModal.tsx | 115 +++--- components/CollectionItemCard.tsx | 272 ++++++------- components/CollectionMap.tsx | 93 +++-- components/InstallPrompt.tsx | 52 +++ components/LocationTracker.tsx | 74 +++- components/OfflineIndicator.tsx | 58 +++ components/PWABridge.tsx | 12 + components/ProblemModal.tsx | 118 +++--- lib/pocketbase.ts | 5 +- lib/pwa.ts | 105 +++++ next.config.js | 5 +- nginx.conf | 39 +- package-lock.json | 594 +++++++++++++++++++++++++++++ package.json | 20 +- public/icons/apple-touch-icon.png | Bin 0 -> 3959 bytes public/icons/icon-128.png | Bin 0 -> 2399 bytes public/icons/icon-144.png | Bin 0 -> 3099 bytes public/icons/icon-152.png | Bin 0 -> 3341 bytes public/icons/icon-192.png | Bin 0 -> 4030 bytes public/icons/icon-384.png | Bin 0 -> 9879 bytes public/icons/icon-512.png | Bin 0 -> 14862 bytes public/icons/icon-72.png | Bin 0 -> 1432 bytes public/icons/icon-96.png | Bin 0 -> 1901 bytes public/icons/icon-maskable-192.png | Bin 0 -> 3835 bytes public/icons/icon-maskable-512.png | Bin 0 -> 14103 bytes public/manifest.json | 80 +++- public/sw.js | 154 ++++++++ scripts/generate-icons.js | 52 +++ 37 files changed, 2110 insertions(+), 707 deletions(-) create mode 100644 components/InstallPrompt.tsx create mode 100644 components/OfflineIndicator.tsx create mode 100644 components/PWABridge.tsx create mode 100644 lib/pwa.ts create mode 100644 public/icons/apple-touch-icon.png create mode 100644 public/icons/icon-128.png create mode 100644 public/icons/icon-144.png create mode 100644 public/icons/icon-152.png create mode 100644 public/icons/icon-192.png create mode 100644 public/icons/icon-384.png create mode 100644 public/icons/icon-512.png create mode 100644 public/icons/icon-72.png create mode 100644 public/icons/icon-96.png create mode 100644 public/icons/icon-maskable-192.png create mode 100644 public/icons/icon-maskable-512.png create mode 100644 public/sw.js create mode 100644 scripts/generate-icons.js diff --git a/.gitignore b/.gitignore index 47c83f0..6b083ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ .next/ dist/ +out/ .env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fafce0f..e35fa07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM node:20-alpine AS builder WORKDIR /app -# Install dependencies +# Install dependencies (including sharp for icon generation) COPY package.json package-lock.json ./ RUN npm ci @@ -15,8 +15,11 @@ COPY . . ARG NEXT_PUBLIC_POCKETBASE_URL ENV NEXT_PUBLIC_POCKETBASE_URL=${NEXT_PUBLIC_POCKETBASE_URL} -# Build the static export -RUN npm run build +# Generate PWA icons +RUN node scripts/generate-icons.js + +# Build the static export (sets NEXT_EXPORT=true) +RUN npm run export # Stage 2: Serve with nginx FROM nginx:alpine @@ -24,14 +27,12 @@ FROM nginx:alpine # Remove default nginx static assets RUN rm -rf /usr/share/nginx/html/* -# Copy built static files from builder -COPY --from=builder /app/dist /usr/share/nginx/html +# Copy built static files from builder (Next.js export outputs to /out) +COPY --from=builder /app/out /usr/share/nginx/html # Copy custom nginx config COPY nginx.conf /etc/nginx/conf.d/default.conf - - EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 33c8fb8..9805b7d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,12 +4,14 @@ :root { --foreground-rgb: 0, 0, 0; - --background-rgb: 245, 245, 245; + --background-rgb: 249, 250, 251; --primary: 234, 88, 12; + --primary-light: 249, 115, 22; --success: 34, 197, 94; --warning: 234, 179, 8; --danger: 239, 68, 68; --info: 6, 182, 212; + --nav-height: 4rem; } html, @@ -18,18 +20,22 @@ body { overflow-x: hidden; height: 100%; -webkit-tap-highlight-color: transparent; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } body { color: rgb(var(--foreground-rgb)); background: rgb(var(--background-rgb)); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; overscroll-behavior-y: none; -webkit-user-select: none; user-select: none; + /* Smooth page transitions */ + scroll-behavior: smooth; } -/* Mobile viewport height fix */ +/* Mobile viewport height fix (100vh is wrong on mobile) */ @supports (-webkit-touch-callout: none) { .h-screen { height: -webkit-fill-available; @@ -48,7 +54,12 @@ body { padding-bottom: env(safe-area-inset-bottom); } -/* Hide scrollbar but keep functionality */ +/* Content must account for bottom nav */ +.pb-nav { + padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom, 0px) + 1rem); +} + +/* Hide scrollbar but keep scroll functionality */ .no-scrollbar::-webkit-scrollbar { display: none; } @@ -62,67 +73,237 @@ body { touch-action: manipulation; } -/* Prevent text selection on buttons */ -button { +/* Prevent text selection on interactive elements */ +button, a, nav { -webkit-user-select: none; user-select: none; } +/* Allow text selection in content areas */ +.selectable { + -webkit-user-select: text; + user-select: text; +} + /* Leaflet container fix */ .leaflet-container { width: 100%; height: 100%; z-index: 1; + font-family: inherit; } -/* Animation utilities */ +/* ======================================== + Animations + ======================================== */ + @keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } -@keyframes slideInRight { - from { transform: translateX(100%); } - to { transform: translateX(0); } +@keyframes slideUp { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } } -@keyframes slideOutRight { - from { transform: translateX(0); } - to { transform: translateX(100%); } +@keyframes slideDown { + from { + transform: translateY(-100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes scaleIn { + from { + transform: scale(0.95); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } } .animate-fade-in { animation: fadeIn 0.3s ease-out forwards; } -/* Status badges */ +.animate-slide-up { + animation: slideUp 0.35s cubic-bezier(0.32, 0.72, 0, 1) forwards; +} + +.animate-slide-down { + animation: slideDown 0.3s ease-out forwards; +} + +.animate-scale-in { + animation: scaleIn 0.2s ease-out forwards; +} + +/* Stagger animation delay helper */ +.stagger-1 { animation-delay: 0.05s; } +.stagger-2 { animation-delay: 0.1s; } +.stagger-3 { animation-delay: 0.15s; } +.stagger-4 { animation-delay: 0.2s; } +.stagger-5 { animation-delay: 0.25s; } + +/* ======================================== + Card Styles + ======================================== */ + +.card-shadow { + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.06), + 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.card-shadow-md { + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); +} + +.card-shadow-lg { + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); +} + +/* Pressed/active state for cards */ +.card-press { + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.card-press:active { + transform: scale(0.985); + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.08); +} + +/* ======================================== + Status Badges + ======================================== */ + .status-pending { @apply bg-gray-100 text-gray-700 border-gray-300; } .status-assigned { @apply bg-blue-100 text-blue-700 border-blue-300; } .status-collected { @apply bg-green-100 text-green-700 border-green-300; } .status-problem { @apply bg-red-100 text-red-700 border-red-300; } -/* Card shadows */ -.card-shadow { - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +/* ======================================== + Loading Skeleton + ======================================== */ + +.skeleton { + background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; } -.card-shadow-lg { - box-shadow: 0 4px 6px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06); +/* ======================================== + Modal Overlay + ======================================== */ + +.modal-overlay { + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); } -/* Pull to refresh indicator */ +/* ======================================== + Input Focus Rings + ======================================== */ + +input:focus, +textarea:focus, +select:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(234, 88, 12, 0.3); +} + +/* ======================================== + Bottom Sheet (iOS-style modal) + ======================================== */ + +.bottom-sheet { + border-radius: 1rem 1rem 0 0; + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); +} + +@media (min-width: 640px) { + .bottom-sheet { + border-radius: 1rem; + } +} + +/* ======================================== + Custom scrollbar (for when needed) + ======================================== */ + +.custom-scrollbar::-webkit-scrollbar { + width: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 2px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.25); +} + +/* ======================================== + Pull-to-refresh visual feedback + ======================================== */ + .pull-indicator { transition: transform 0.2s ease-out; } -/* Loading skeleton */ -.skeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: skeleton-loading 1.5s infinite; +/* ======================================== + Haptic-like press feedback + ======================================== */ + +.press-feedback { + transition: transform 0.1s ease, opacity 0.1s ease; } -@keyframes skeleton-loading { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } +.press-feedback:active { + transform: scale(0.96); + opacity: 0.9; } + +.press-feedback-sm { + transition: transform 0.1s ease, opacity 0.1s ease; +} + +.press-feedback-sm:active { + transform: scale(0.98); + opacity: 0.9; +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 5652308..99ff85d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,16 +1,22 @@ import type { Metadata, Viewport } from "next"; import "./globals.css"; import { AuthProvider } from "@/components/AuthProvider"; +import OfflineIndicator from "@/components/OfflineIndicator"; +import InstallPrompt from "@/components/InstallPrompt"; +import { PWABridge } from "@/components/PWABridge"; export const metadata: Metadata = { title: "1Driver - Collections", - description: "Mobile driver collection app", + description: "Mobile driver collection management app", manifest: "/manifest.json", appleWebApp: { capable: true, statusBarStyle: "black-translucent", title: "1Driver", }, + formatDetection: { + telephone: false, + }, }; export const viewport: Viewport = { @@ -19,7 +25,7 @@ export const viewport: Viewport = { maximumScale: 1, userScalable: false, viewportFit: "cover", - themeColor: "#ea580c", + themeColor: "#ea580c", }; export default function RootLayout({ @@ -30,17 +36,25 @@ export default function RootLayout({ return ( - + + + + + + - + + + + {children} ); -} +} \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx index 15ef9b7..ea253fc 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -19,7 +19,6 @@ export default function LoginPage() { try { await login(email, password); - // Auth provider will detect the change and redirect window.location.href = '/'; } catch (err) { setError('Invalid email or password'); @@ -29,85 +28,92 @@ export default function LoginPage() { }; return ( -
-
- {/* Logo */} +
+
+ {/* Logo area */}
-
- +
+
-

1Driver

-

Collections App

+

1Driver

+

Collection Management

-
-
- - setEmail(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all" - placeholder="driver@example.com" - required - autoFocus - autoCapitalize="off" - autoCorrect="off" - /> -
- -
- -
+ {/* Card */} +
+ +
+ setPassword(e.target.value)} - className="w-full p-3 pr-12 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all" - placeholder="Enter password" + type="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + className="w-full p-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-orange-500/30 focus:border-orange-500 focus:bg-white transition-all" + placeholder="driver@example.com" required + autoFocus + autoCapitalize="off" + autoCorrect="off" /> -
-
- {error && ( -
-

{error}

+
+ +
+ setPassword(e.target.value)} + className="w-full p-3.5 pr-12 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-orange-500/30 focus:border-orange-500 focus:bg-white transition-all" + placeholder="Enter password" + required + /> + +
- )} -
)} - > - {isLoading ? ( - <> -
- Signing in... - - ) : ( - 'Sign In' - )} - - + + + +
+ +

+ Contact your administrator for account access +

); -} +} \ No newline at end of file diff --git a/app/map/page.tsx b/app/map/page.tsx index 84b91a8..76629a6 100644 --- a/app/map/page.tsx +++ b/app/map/page.tsx @@ -8,14 +8,17 @@ import { useAuth } from '@/components/AuthProvider'; import BottomNav from '@/components/BottomNav'; import LocationTracker from '@/components/LocationTracker'; import { getAssignedCollections, isAuthenticated } from '@/lib/pocketbase'; -import { cn, parseCoordinates } from '@/lib/utils'; +import { cn } from '@/lib/utils'; import { RefreshCw, MapPin, AlertCircle } from 'lucide-react'; const CollectionMap = dynamic(() => import('@/components/CollectionMap'), { ssr: false, loading: () => (
-
+
+
+

Loading map...

+
), }); @@ -54,14 +57,17 @@ export default function MapPage() { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); const url = isIOS ? `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}` - : `https://www.google.com/maps?q=${lat},${lng}`; + : `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`; window.open(url, '_blank'); }; if (authLoading) { return (
-
+
+
+

Loading...

+
); } @@ -72,46 +78,57 @@ export default function MapPage() {
-
+
-
- -

Collection Map

-
-
- {items.length} stops - +
+
+ +
+
+

Map

+

{items.length} stop{items.length !== 1 ? 's' : ''}

+
+
{isLoading ? (
-
+
+
+

Loading map...

+
) : error ? (
- +
+ +

{error}

- +
) : items.length === 0 ? (
- -

No collections to show

+
+ +
+

No collections to show

All collections completed!

) : ( -
+
}> @@ -122,4 +139,4 @@ export default function MapPage() {
); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 1686a2f..f4e0d4e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -23,10 +23,10 @@ import { MapPin, AlertCircle, CheckCircle2, - Clock, - Package, Search, X, + Package, + ListFilter, } from 'lucide-react'; export default function CollectionsPage() { @@ -42,12 +42,9 @@ export default function CollectionsPage() { const [showCollectModal, setShowCollectModal] = useState(false); const [showProblemModal, setShowProblemModal] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); - const [filter, setFilter] = useState<'all' | 'today' | 'all_date'>('all'); useEffect(() => { - if (!authLoading && !isLoggedIn) { - router.push('/login/'); - } + if (!authLoading && !isLoggedIn) router.push('/login/'); }, [isLoggedIn, authLoading, router]); const loadItems = useCallback(async (showLoading = true) => { @@ -61,16 +58,14 @@ export default function CollectionsPage() { setFilteredItems(data); } catch (err) { console.error('Failed to load collections:', err); - setError('Failed to load collections. Please try again.'); + setError('Failed to load collections. Pull down to retry.'); } finally { setIsLoading(false); setIsRefreshing(false); } }, []); - useEffect(() => { - loadItems(); - }, [loadItems]); + useEffect(() => { loadItems(); }, [loadItems]); useEffect(() => { let filtered = items; @@ -109,7 +104,6 @@ export default function CollectionsPage() { notes, collected_date: new Date().toISOString(), }); - setItems((prev) => prev.filter((i) => i.id !== item.id)); setShowCollectModal(false); showToast('Collection completed successfully'); @@ -119,12 +113,7 @@ export default function CollectionsPage() { } }; - const handleProblem = async ( - item: OrderItem, - problemType: string, - description: string, - photo?: File - ) => { + const handleProblem = async (item: OrderItem, problemType: string, description: string) => { try { await reportProblem(item.id, problemType, description); setItems((prev) => prev.filter((i) => i.id !== item.id)); @@ -140,7 +129,7 @@ export default function CollectionsPage() { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); const url = isIOS ? `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}` - : `https://www.google.com/maps?q=${lat},${lng}`; + : `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`; window.open(url, '_blank'); }; @@ -163,7 +152,10 @@ export default function CollectionsPage() { if (authLoading) { return (
-
+
+
+

Loading...

+
); } @@ -171,106 +163,135 @@ export default function CollectionsPage() { if (!isLoggedIn) return null; return ( -
+
-
-
+ {/* Header */} +
+
-
-

Collections

-

{user?.name || user?.email}

+
+

Collections

+

{user?.name || user?.email}

-
- - +
+
+ {/* Search bar */}
- + setSearchQuery(e.target.value)} - placeholder="Search collections..." - className="w-full pl-9 pr-9 py-2 bg-white/10 border border-white/20 rounded-xl text-sm text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30" + placeholder="Search by item, supplier, order..." + className="w-full pl-9 pr-9 py-2.5 bg-white/15 backdrop-blur-sm border border-white/20 rounded-xl text-sm text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-white/30 focus:bg-white/20 transition-all" /> {searchQuery && ( - )}
-
- {[ - { key: 'all' as const, label: 'All', icon: Package }, - ].map(({ key, label, icon: Icon }) => ( - - ))} + {/* Stats pill */} +
+ + + {items.length} item{items.length !== 1 ? 's' : ''} + + {groupedArray.length > 0 && ( + + + {groupedArray.length} stop{groupedArray.length !== 1 ? 's' : ''} + + )}
-
+ {/* Content */} +
{isLoading ? (
- {[...Array(5)].map((_, i) => ( -
-
-
-
+ {[...Array(4)].map((_, i) => ( +
+
+
+
))}
) : error ? ( -
- -

{error}

-
) : groupedArray.length === 0 ? ( -
- -

All caught up!

-

- {searchQuery ? 'No collections match your search' : 'No assigned collections'} +

+
+ +
+

All caught up!

+

+ {searchQuery ? 'No collections match your search' : 'No assigned collections right now'}

) : (
- {groupedArray.map(({ supplier, items }) => ( -
-
- -

{supplier!.suppliername}

- {items.length} - {supplier!.supplieraddress && ( - {supplier!.supplieraddress} - )} + {groupedArray.map(({ supplier, items: supplierItems }, groupIdx) => ( +
+ {/* Supplier header */} +
+
+ +
+
+

{supplier!.suppliername}

+ {supplier!.supplieraddress && ( +

{supplier!.supplieraddress}

+ )} +
+ + {supplierItems.length} +
-
- {items.map((item) => ( + + {/* Items */} +
+ {supplierItems.map((item) => ( + {/* Modals */} + {/* Toast */} {toast && ( -
+
); -} +} \ No newline at end of file diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 9992447..b59d308 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -11,29 +11,29 @@ import { Settings, Clock, Crosshair, - Moon, - Type, - WifiOff, LogOut, - ChevronRight, Save, RotateCcw, Info, + ChevronRight, + Signal, + MapPin, + Shield, } 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' }, + { 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 (High)' }, - { value: 50, label: '50m (Balanced)' }, - { value: 100, label: '100m (Battery)' }, - { value: 500, label: '500m (Low)' }, + { 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() { @@ -43,9 +43,7 @@ export default function SettingsPage() { const [saved, setSaved] = useState(false); useEffect(() => { - if (!authLoading && !isLoggedIn) { - router.push('/login/'); - } + if (!authLoading && !isLoggedIn) router.push('/login/'); }, [isLoggedIn, authLoading, router]); useEffect(() => { @@ -72,29 +70,28 @@ export default function SettingsPage() { if (authLoading) { return (
-
+
+
+

Loading...

+
); } - if (!isLoggedIn) { - return null; - } + if (!isLoggedIn) return null; return ( -
+
{/* Header */}
-
- +
+
-
+

Settings

-

- {user?.name || user?.email} -

+

{user?.name || user?.email}

@@ -104,190 +101,129 @@ export default function SettingsPage() { {/* Location Settings */}
- -

+ +

Location Tracking

-
+
{/* Update Interval */} -
+
- - Update Interval - + Ping Interval
- - {settings.locationInterval}s + + {settings.locationInterval >= 60 ? `${settings.locationInterval / 60} min` : `${settings.locationInterval}s`}
-
+
{INTERVAL_OPTIONS.map((option) => ( ))}
-

- How often your location is sent to the server -

{/* Minimum Accuracy */} -
+
- - Minimum Accuracy - + Accuracy Filter
- + {settings.minimumAccuracy}m
-
+
{ACCURACY_OPTIONS.map((option) => ( ))}
-

- Skip location updates with worse accuracy -

{/* High Accuracy Toggle */}
-
- +
+
+ +
- - High Accuracy Mode - - - Uses GPS for better precision - + High Accuracy GPS + Uses more battery for precision
-
-
- {/* Display Settings */} -
-
- -

- Display -

-
- -
- {/* Font Size */} -
-
- - Font Size -
-
- {(['small', 'normal', 'large'] as const).map((size) => ( - - ))} -
-
- - {/* Offline Mode */} -
+ {/* Screen Wake Lock */} +
-
- +
+
+ +
- - Offline Queue - - - Queue actions when offline - + Keep Screen On + Prevents phone from sleeping
@@ -297,31 +233,30 @@ export default function SettingsPage() { {/* About */}
- -

- About -

+ +

About

-
-
-

1Driver

+
+
+
+ 1D +
+

1Driver

Version 1.0.0

-

- Collections Management App -

+

Collection Management App

{/* Actions */} -
+
-
-

{item.description}

-

- Expected: {expectedQty} units -

- {item.expand?.order && ( -

- Order: {item.expand.order.order_number} -

- )} + {/* Item info */} +
+

{item.description}

+
+ + {item.expand?.supplier?.suppliername || 'Unknown'} + + Expected: {expectedQty} units +
{item.selling_price && ( -

+

Unit price: {formatCurrency(item.selling_price)}

)}
-
-
-