ultisuite-client/components/gmail/sync-status-bar.tsx
R3D347HR4Y c87670e90f
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
feat(api): offline-first mail sync w/ TanStack Query
Move mail, compose, contacts, and accounts off mocks onto REST + WS.
Add client, auth store, IDB-backed query cache, offline queue, and
sync bar; hybrid Zustand for UI-only state. Settings still local until
backend has preferences API.
2026-05-23 00:04:28 +02:00

95 lines
2.6 KiB
TypeScript

"use client"
import { useNetworkStatus } from "@/lib/api/use-network-status"
import { useEffect, useState, useCallback } from "react"
import { Icon } from "@iconify/react"
import { getPendingCount, flush } from "@/lib/api/offline-queue"
import { cn } from "@/lib/utils"
type SyncState = "idle" | "offline" | "syncing"
export function SyncStatusBar() {
const { isOnline } = useNetworkStatus()
const [syncState, setSyncState] = useState<SyncState>("idle")
const [pendingCount, setPendingCount] = useState(0)
const refreshCount = useCallback(async () => {
const count = await getPendingCount()
setPendingCount(count)
return count
}, [])
useEffect(() => {
if (!isOnline) {
setSyncState("offline")
return
}
let cancelled = false
const syncOnReconnect = async () => {
const count = await refreshCount()
if (cancelled) return
if (count > 0) {
setSyncState("syncing")
await flush()
if (cancelled) return
await refreshCount()
setSyncState("idle")
} else {
setSyncState("idle")
}
}
syncOnReconnect()
return () => {
cancelled = true
}
}, [isOnline, refreshCount])
useEffect(() => {
if (syncState !== "offline") return
const interval = setInterval(() => refreshCount(), 2000)
return () => clearInterval(interval)
}, [syncState, refreshCount])
const visible = syncState !== "idle"
return (
<div
className={cn(
"overflow-hidden transition-all duration-300 ease-in-out",
visible ? "max-h-8 opacity-100" : "max-h-0 opacity-0"
)}
>
<div
className={cn(
"flex h-8 items-center justify-center gap-2 px-3 text-xs font-medium",
syncState === "offline" &&
"bg-amber-50 text-amber-800 dark:bg-amber-950/50 dark:text-amber-200",
syncState === "syncing" &&
"bg-blue-50 text-blue-800 dark:bg-blue-950/50 dark:text-blue-200"
)}
>
{syncState === "offline" && (
<>
<Icon icon="mdi:wifi-off" className="size-3.5" />
<span>Offline changes will sync when reconnected</span>
{pendingCount > 0 && (
<span className="rounded-full bg-amber-200/60 px-1.5 py-0.5 text-[10px] dark:bg-amber-800/40">
{pendingCount} pending
</span>
)}
</>
)}
{syncState === "syncing" && (
<>
<Icon icon="mdi:sync" className="size-3.5 animate-spin" />
<span>Syncing {pendingCount} changes</span>
</>
)}
</div>
</div>
)
}