1 import React, { useState, useRef } from "react";
2 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3 import { Button } from "@/components/ui/button";
4 import { Badge } from "@/components/ui/badge";
5 import { Progress } from "@/components/ui/progress";
6 import { Input } from "@/components/ui/input";
7 import { useToast } from "@/hooks/use-toast";
8 import { useOfflineMode } from "@/hooks/useOfflineMode";
9 import { useAuth } from "@/hooks/useAuth";
10 import { useTheme } from "@/contexts/theme-context";
11 import { formatStorageSize } from "@/lib/offline-storage";
12 import { getApiBaseUrl, setApiBaseUrl } from "@/lib/config";
13 import {
14 Download,
15 Upload,
16 Wifi,
17 WifiOff,
18 HardDrive,
19 RefreshCw,
20 CheckCircle,
21 AlertCircle,
22 Clock,
23 Settings,
24 Server,
25 Globe,
26 User as UserIcon,
27 Camera,
28 LogOut,
29 Moon,
30 Sun,
31 Smartphone
32 } from "lucide-react";
33 import { motion } from "framer-motion";
34 import { apiRequest } from "@/lib/queryClient";
35
36 export default function OfflineSettings() {
37 const { toast } = useToast();
38 const { user, logoutMutation } = useAuth();
39 const { theme, setTheme } = useTheme();
40 const fileInputRef = useRef<HTMLInputElement>(null);
41 const [uploading, setUploading] = useState(false);
42
43 const {
44 isOfflineSupported,
45 isOfflineModeEnabled,
46 isCurrentlyOnline,
47 lastSyncTime,
48 storageInfo,
49 enableOfflineMode,
50 disableOfflineMode,
51 forceSync,
52 getOfflineData,
53 } = useOfflineMode();
54
55 const [serverUrl, setServerUrl] = useState(getApiBaseUrl());
56
57 const handleEnableOffline = async () => {
58 const success = await enableOfflineMode();
59 if (success) {
60 toast({ title: "Offline Mode Enabled", description: "Repair guides cached." });
61 } else {
62 toast({ title: "Failed", description: "Check connection.", variant: "destructive" });
63 }
64 };
65
66 const handleSaveServer = () => {
67 setApiBaseUrl(serverUrl);
68 toast({ title: "Saved", description: "Reloading..." });
69 setTimeout(() => window.location.reload(), 1000);
70 };
71
72 const handlePhotoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
73 const file = e.target.files?.[0];
74 if (!file) return;
75
76 setUploading(true);
77 const formData = new FormData();
78 formData.append("photo", file);
79
80 try {
81 const res = await fetch("/api/user/profile/photo", {
82 method: "POST",
83 body: formData,
84 });
85 if (res.ok) {
86 toast({ title: "Success", description: "Profile photo updated." });
87 window.location.reload();
88 } else {
89 throw new Error();
90 }
91 } catch (e) {
92 toast({ title: "Error", description: "Upload failed.", variant: "destructive" });
93 } finally {
94 setUploading(false);
95 }
96 };
97
98 const offlineData = getOfflineData();
99
100 return (
101 <div className="max-w-4xl mx-auto px-4 py-10 space-y-8">
102
103 {/* Profile Header */}
104 <section className="flex flex-col md:flex-row items-center gap-8 bg-card/40 backdrop-blur-2xl p-8 rounded-[3rem] border border-border/40">
105 <div className="relative group">
106 <div className="h-32 w-32 rounded-[2.5rem] bg-primary/10 border-2 border-primary/20 overflow-hidden flex items-center justify-center">
107 {user?.profileImageUrl ? (
108 <img src={user.profileImageUrl} className="h-full w-full object-cover" alt="Me" />
109 ) : (
110 <UserIcon className="h-12 w-12 text-primary" />
111 )}
112 </div>
113 <button
114 onClick={() => fileInputRef.current?.click()}
115 className="absolute -bottom-2 -right-2 h-10 w-10 rounded-full bg-primary text-white flex items-center justify-center shadow-lg hover:scale-110 transition-transform"
116 disabled={uploading}
117 >
118 {uploading ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Camera className="h-4 w-4" />}
119 </button>
120 <input
121 type="file"
122 ref={fileInputRef}
123 className="hidden"
124 accept="image/*"
125 onChange={handlePhotoUpload}
126 />
127 </div>
128
129 <div className="flex-1 text-center md:text-left">
130 <h1 className="text-3xl font-black tracking-tight">{user?.firstName} {user?.lastName}</h1>
131 <p className="text-muted-foreground font-medium mb-4">{user?.email}</p>
132 <div className="flex flex-wrap justify-center md:justify-start gap-2">
133 <Badge variant="outline" className="bg-primary/5 text-primary border-primary/20 px-3 py-1">Expert Tech</Badge>
134 <Badge variant="outline" className="bg-emerald-500/5 text-emerald-500 border-emerald-500/20 px-3 py-1">Verified</Badge>
135 </div>
136 </div>
137
138 <Button
139 variant="ghost"
140 onClick={() => logoutMutation.mutate()}
141 className="text-destructive hover:bg-destructive/10 rounded-2xl px-6 gap-2"
142 >
143 <LogOut className="h-4 w-4" /> Sign Out
144 </Button> section
145 </section>
146
147 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
148
149 {/* Appearance Settings */}
150 <Card className="rounded-[2.5rem] border-border/40 shadow-glass bg-card/20">
151 <CardHeader>
152 <CardTitle className="flex items-center gap-2">
153 <Settings className="h-5 w-5 text-indigo-400" /> Appearance
154 </CardTitle>
155 </CardHeader>
156 <CardContent className="space-y-4">
157 <div className="grid grid-cols-3 gap-2">
158 <Button
159 variant={theme === "light" ? "default" : "outline"}
160 onClick={() => setTheme("light")}
161 className="rounded-2xl h-20 flex-col gap-2"
162 >
163 <Sun className="h-5 w-5" /> Light
164 </Button>
165 <Button
166 variant={theme === "dark" ? "default" : "outline"}
167 onClick={() => setTheme("dark")}
168 className="rounded-2xl h-20 flex-col gap-2"
169 >
170 <Moon className="h-5 w-5" /> Dark
171 </Button>
172 <Button
173 variant={theme === "amoled" ? "default" : "outline"}
174 onClick={() => setTheme("amoled")}
175 className="rounded-2xl h-20 flex-col gap-2"
176 >
177 <Smartphone className="h-5 w-5" /> AMOLED
178 </Button>
179 </div>
180 </CardContent>
181 </Card>
182
183 {/* Server Settings */}
184 <Card className="rounded-[2.5rem] border-border/40 shadow-glass bg-card/20">
185 <CardHeader>
186 <CardTitle className="flex items-center gap-2">
187 <Server className="h-5 w-5 text-emerald-400" /> Connectivity
188 </CardTitle>
189 </CardHeader>
190 <CardContent className="space-y-4">
191 <div className="space-y-2">
192 <label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground">API Base URL</label>
193 <div className="flex gap-2">
194 <Input
195 value={serverUrl}
196 onChange={(e) => setServerUrl(e.target.value)}
197 className="rounded-2xl bg-muted/30 border-none h-11"
198 />
199 <Button onClick={handleSaveServer} className="rounded-2xl h-11">Save</Button>
200 </div>
201 </div>
202 </CardContent>
203 </Card>
204
205 {/* Offline Mode */}
206 <Card className="md:col-span-2 rounded-[2.5rem] border-border/40 shadow-glass bg-card/20">
207 <CardHeader>
208 <div className="flex items-center justify-between">
209 <div>
210 <CardTitle className="flex items-center gap-2">
211 <HardDrive className="h-5 w-5 text-amber-400" /> Offline Systems
212 </CardTitle>
213 <CardDescription>Cache technical data for field use</CardDescription>
214 </div>
215 <Badge variant={isCurrentlyOnline ? "default" : "destructive"} className="rounded-full">
216 {isCurrentlyOnline ? <Wifi className="h-3 w-3 mr-1" /> : <WifiOff className="h-3 w-3 mr-1" />}
217 {isCurrentlyOnline ? "Sync Ready" : "Local Only"}
218 </Badge>
219 </div>
220 </CardHeader>
221 <CardContent className="space-y-6">
222 <div className="flex items-center justify-between p-6 rounded-3xl bg-muted/20 border border-border/20">
223 <div>
224 <h3 className="font-bold">Offline Database</h3>
225 <p className="text-xs text-muted-foreground">{isOfflineModeEnabled ? "Fully cached for field ops" : "Field access disabled"}</p>
226 </div>
227 <div className="flex gap-2">
228 {isOfflineModeEnabled ? (
229 <Button variant="outline" onClick={disableOfflineMode} className="rounded-2xl">Disable</Button>
230 ) : (
231 <Button onClick={handleEnableOffline} disabled={!isCurrentlyOnline} className="rounded-2xl bg-primary">Enable Field Access</Button>
232 )}
233 {isOfflineModeEnabled && (
234 <Button variant="ghost" onClick={forceSync} disabled={!isCurrentlyOnline} className="rounded-full h-10 w-10 p-0">
235 <RefreshCw className="h-4 w-4" />
236 </Button>
237 )}
238 </div>
239 </div>
240
241 {isOfflineModeEnabled && (
242 <div className="space-y-2">
243 <div className="flex justify-between text-xs font-bold uppercase tracking-widest text-muted-foreground">
244 <span>Local Cache Usage</span>
245 <span>{storageInfo.percentage.toFixed(1)}%</span>
246 </div>
247 <Progress value={storageInfo.percentage} className="h-2 rounded-full" />
248 <div className="grid grid-cols-3 gap-4 mt-4">
249 <div className="p-4 rounded-2xl bg-muted/10 text-center">
250 <div className="text-xl font-black">{offlineData.guides.length}</div>
251 <div className="text-[10px] text-muted-foreground uppercase">Guides</div>
252 </div>
253 <div className="p-4 rounded-2xl bg-muted/10 text-center">
254 <div className="text-xl font-black">{offlineData.flows.length}</div>
255 <div className="text-[10px] text-muted-foreground uppercase">Flows</div>
256 </div>
257 <div className="p-4 rounded-2xl bg-muted/10 text-center">
258 <div className="text-xl font-black">{offlineData.components.length}</div>
259 <div className="text-[10px] text-muted-foreground uppercase">Parts</div>
260 </div>
261 </div>
262 </div>
263 )}
264 </CardContent>
265 </Card>
266 </div>
267 </div>
268 );
269 }