| // src/components/SystemStatusBar.tsx | |
| import { useEffect, useMemo, useState } from "react"; | |
| import { | |
| Cloud, CloudOff, Loader2, Radio, ShieldCheck, Wifi, WifiOff, | |
| CheckCircle, AlertCircle, RefreshCw, Zap, Battery, Activity as Signal, | |
| Activity, Database, Server, Globe, Lock, Eye, EyeOff | |
| } from "lucide-react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { useAuth } from "@/context/AuthContext"; | |
| import { useOffline } from "@/context/OfflineContext"; | |
| import { useWebSocket } from "@/context/WebSocketContext"; | |
| function getBrowserOnline() { | |
| return typeof navigator === "undefined" ? true : navigator.onLine; | |
| } | |
| interface SystemStatusBarProps { | |
| showDetails?: boolean; | |
| compact?: boolean; | |
| onStatusClick?: () => void; | |
| } | |
| export function SystemStatusBar({ showDetails = true, compact = false, onStatusClick }: SystemStatusBarProps) { | |
| const { isOfflineMode } = useAuth(); | |
| const { isOnline, queueLength, processQueue, syncProgress, lastSyncTime } = useOffline(); | |
| const { connectionStatus, reconnect, pingLatency } = useWebSocket(); | |
| const [browserOnline, setBrowserOnline] = useState(getBrowserOnline); | |
| const [showTooltip, setShowTooltip] = useState(false); | |
| useEffect(() => { | |
| const update = () => setBrowserOnline(getBrowserOnline()); | |
| update(); | |
| window.addEventListener("online", update); | |
| window.addEventListener("offline", update); | |
| return () => { | |
| window.removeEventListener("online", update); | |
| window.removeEventListener("offline", update); | |
| }; | |
| }, []); | |
| const status = useMemo(() => { | |
| if (isOfflineMode) { | |
| return { | |
| label: "Offline session", | |
| detail: queueLength > 0 ? `${queueLength} queued actions` : "Saved login session", | |
| icon: WifiOff, | |
| color: "amber", | |
| actions: queueLength > 0 ? `${queueLength} pending` : null, | |
| }; | |
| } | |
| if (!browserOnline || !isOnline) { | |
| return { | |
| label: "No connection", | |
| detail: queueLength > 0 ? `${queueLength} queued actions` : "Local mode active", | |
| icon: WifiOff, | |
| color: "red", | |
| actions: queueLength > 0 ? "Retry sync" : null, | |
| }; | |
| } | |
| if (connectionStatus === "connecting") { | |
| return { | |
| label: "Connecting", | |
| detail: queueLength > 0 ? `${queueLength} queued actions` : "Establishing secure connection", | |
| icon: Loader2, | |
| color: "amber", | |
| actions: "Connecting...", | |
| }; | |
| } | |
| if (connectionStatus === "error" || connectionStatus === "disconnected") { | |
| return { | |
| label: "Live updates off", | |
| detail: "Real-time chat unavailable. REST API still works.", | |
| icon: CloudOff, | |
| color: "amber", | |
| actions: "Reconnect", | |
| }; | |
| } | |
| if (syncProgress && syncProgress < 100) { | |
| return { | |
| label: "Syncing", | |
| detail: `${Math.round(syncProgress)}% complete`, | |
| icon: RefreshCw, | |
| color: "blue", | |
| actions: `${Math.round(syncProgress)}%`, | |
| }; | |
| } | |
| return { | |
| label: "Online", | |
| detail: queueLength > 0 ? `Syncing ${queueLength} items` : "All systems operational", | |
| icon: Wifi, | |
| color: "green", | |
| actions: queueLength > 0 ? `${queueLength} pending` : null, | |
| }; | |
| }, [browserOnline, connectionStatus, isOfflineMode, isOnline, queueLength, syncProgress]); | |
| const getColorClasses = () => { | |
| const colors = { | |
| green: "premium-glass border-emerald-400/30 bg-emerald-100/40 text-emerald-800 dark:border-emerald-500/20 dark:bg-emerald-950/40 dark:text-emerald-300", | |
| amber: "premium-glass border-amber-400/30 bg-amber-100/40 text-amber-800 dark:border-amber-500/20 dark:bg-amber-950/40 dark:text-amber-300", | |
| red: "premium-glass border-red-400/30 bg-red-100/40 text-red-800 dark:border-red-500/20 dark:bg-red-950/40 dark:text-red-300", | |
| blue: "premium-glass border-blue-400/30 bg-blue-100/40 text-blue-800 dark:border-blue-500/20 dark:bg-blue-950/40 dark:text-blue-300", | |
| }; | |
| return colors[status.color as keyof typeof colors] || colors.green; | |
| }; | |
| const StatusIcon = status.icon; | |
| const canRetry = browserOnline && isOnline && (connectionStatus === "disconnected" || connectionStatus === "error" || queueLength > 0); | |
| const handleRetry = () => { | |
| if (connectionStatus === "disconnected" || connectionStatus === "error") reconnect(); | |
| if (queueLength > 0) void processQueue(); | |
| }; | |
| if (compact) { | |
| return ( | |
| <div | |
| className={`relative flex items-center gap-1.5 rounded-full px-2 py-1 text-xs transition-all cursor-pointer hover:scale-105 ${getColorClasses()}`} | |
| onClick={onStatusClick || handleRetry} | |
| onMouseEnter={() => setShowTooltip(true)} | |
| onMouseLeave={() => setShowTooltip(false)} | |
| > | |
| <StatusIcon className={`h-3 w-3 ${connectionStatus === "connecting" ? "animate-spin" : ""}`} /> | |
| <span className="hidden sm:inline"> | |
| {status.label} {pingLatency !== null && `(${pingLatency}ms)`} | |
| </span> | |
| {showTooltip && ( | |
| <div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-2 py-1 bg-gray-900 text-white text-xs rounded whitespace-nowrap z-50 shadow-lg"> | |
| {status.detail} {pingLatency !== null && `| Latency: ${pingLatency}ms`} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| if (status.label === "Online" && !syncProgress) { | |
| return null; // Hide normal status bar if fully connected | |
| } | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0, y: -10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className={`flex min-w-0 items-center gap-2 rounded-lg border px-3 py-2 text-xs shadow-sm transition-all cursor-pointer hover:shadow-md ${getColorClasses()}`} | |
| onClick={onStatusClick || handleRetry} | |
| role="status" | |
| aria-live="polite" | |
| > | |
| <StatusIcon className={`h-4 w-4 shrink-0 ${connectionStatus === "connecting" || syncProgress ? "animate-spin" : ""}`} /> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2 font-medium leading-none"> | |
| <span>{status.label}</span> | |
| <span className="text-current/30">/</span> | |
| <span className="truncate text-current/70">{status.detail}</span> | |
| </div> | |
| {showDetails && ( | |
| <div className="flex items-center gap-3 mt-1 text-[10px] text-current/60"> | |
| {pingLatency && ( | |
| <span className="flex items-center gap-0.5"> | |
| <Zap className="h-2.5 w-2.5" /> {pingLatency}ms | |
| </span> | |
| )} | |
| {lastSyncTime && ( | |
| <span className="flex items-center gap-0.5"> | |
| <Database className="h-2.5 w-2.5" /> Last sync: {new Date(lastSyncTime).toLocaleTimeString()} | |
| </span> | |
| )} | |
| {connectionStatus === "connected" && ( | |
| <span className="flex items-center gap-0.5"> | |
| <ShieldCheck className="h-2.5 w-2.5" /> Secure connection | |
| </span> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| {/* Status Icons */} | |
| <div className="hidden items-center gap-1.5 text-current/60 lg:flex"> | |
| {isOfflineMode ? ( | |
| <Lock className="h-3.5 w-3.5" /> | |
| ) : connectionStatus === "connected" ? ( | |
| <> | |
| <Radio className="h-3.5 w-3.5" /> | |
| <Cloud className="h-3.5 w-3.5" /> | |
| <ShieldCheck className="h-3.5 w-3.5" /> | |
| {queueLength === 0 && <CheckCircle className="h-3.5 w-3.5 text-green-500" />} | |
| </> | |
| ) : ( | |
| <AlertCircle className="h-3.5 w-3.5" /> | |
| )} | |
| </div> | |
| {/* Action Button */} | |
| {canRetry && ( | |
| <button | |
| type="button" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleRetry(); | |
| }} | |
| className="hidden rounded-md px-2 py-0.5 text-xs font-medium text-current/80 hover:bg-black/5 dark:hover:bg-white/10 md:inline-flex items-center gap-1" | |
| > | |
| <RefreshCw className="h-3 w-3" /> Retry | |
| </button> | |
| )} | |
| {/* Progress Indicator */} | |
| {syncProgress && syncProgress < 100 && ( | |
| <div className="w-16 h-1 bg-current/20 rounded-full overflow-hidden"> | |
| <motion.div | |
| className="h-full bg-current rounded-full" | |
| initial={{ width: 0 }} | |
| animate={{ width: `${syncProgress}%` }} | |
| transition={{ duration: 0.3 }} | |
| /> | |
| </div> | |
| )} | |
| </motion.div> | |
| ); | |
| } | |
| // Small version for mobile | |
| export function MobileStatusIndicator() { | |
| const { connectionStatus } = useWebSocket(); | |
| const { isOnline } = useOffline(); | |
| const isConnected = connectionStatus === "connected" && isOnline; | |
| return ( | |
| <div className="flex items-center gap-1"> | |
| <div className={`h-2 w-2 rounded-full ${isConnected ? "bg-green-500 animate-pulse" : "bg-red-500"}`} /> | |
| <span className="text-xs text-gray-500"> | |
| {isConnected ? "Live" : "Offline"} | |
| </span> | |
| </div> | |
| ); | |
| } | |
| // Export all | |
| export default SystemStatusBar; | |
Xet Storage Details
- Size:
- 9.03 kB
- Xet hash:
- 21e05379e636323f347a788381ceace5e27739d78a68449316d88f0815673309
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.