103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { WifiOff, CloudUpload, RefreshCw } from "lucide-react";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { useSyncQueueStore } from "@/lib/stores/sync-queue.store";
|
||
|
|
import { useLocale } from "next-intl";
|
||
|
|
import {
|
||
|
|
getAllQueueItems,
|
||
|
|
getQueueCount,
|
||
|
|
removeQueueItem,
|
||
|
|
markQueueItemFailed,
|
||
|
|
} from "@/lib/offline/offline-db";
|
||
|
|
import { apiPost } from "@/lib/api/client";
|
||
|
|
|
||
|
|
/** Manual retry — fires one sync pass immediately (used as onClick). */
|
||
|
|
async function runManualSync(
|
||
|
|
setSyncing: (v: boolean) => void,
|
||
|
|
setQueueCount: (n: number) => void
|
||
|
|
) {
|
||
|
|
if (!navigator.onLine) return;
|
||
|
|
setSyncing(true);
|
||
|
|
try {
|
||
|
|
const items = await getAllQueueItems();
|
||
|
|
for (const item of items) {
|
||
|
|
try {
|
||
|
|
if (item.type === "create_order") {
|
||
|
|
const { cafeId, body } = item.payload as { cafeId: string; body: unknown };
|
||
|
|
await apiPost(`/api/cafes/${cafeId}/orders`, body as Record<string, unknown>);
|
||
|
|
} else if (item.type === "add_items") {
|
||
|
|
const { cafeId, orderId, body } = item.payload as {
|
||
|
|
cafeId: string;
|
||
|
|
orderId: string;
|
||
|
|
body: unknown;
|
||
|
|
};
|
||
|
|
await apiPost(
|
||
|
|
`/api/cafes/${cafeId}/orders/${orderId}/items`,
|
||
|
|
body as Record<string, unknown>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
await removeQueueItem(item.id);
|
||
|
|
} catch {
|
||
|
|
await markQueueItemFailed(item.id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
setSyncing(false);
|
||
|
|
setQueueCount(await getQueueCount());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SyncStatusIndicator() {
|
||
|
|
const { queueCount, isSyncing, isOnline, setSyncing, setQueueCount } =
|
||
|
|
useSyncQueueStore();
|
||
|
|
const locale = useLocale();
|
||
|
|
const isFa = locale !== "en";
|
||
|
|
|
||
|
|
const show = !isOnline || queueCount > 0 || isSyncing;
|
||
|
|
if (!show) return null;
|
||
|
|
|
||
|
|
const label = isFa
|
||
|
|
? !isOnline
|
||
|
|
? "آفلاین"
|
||
|
|
: isSyncing
|
||
|
|
? "همگامسازی..."
|
||
|
|
: `${queueCount} مورد در صف`
|
||
|
|
: !isOnline
|
||
|
|
? "Offline"
|
||
|
|
: isSyncing
|
||
|
|
? "Syncing..."
|
||
|
|
: `${queueCount} pending`;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => void runManualSync(setSyncing, setQueueCount)}
|
||
|
|
disabled={isSyncing || !isOnline}
|
||
|
|
title={
|
||
|
|
isFa
|
||
|
|
? "برای همگامسازی دستی کلیک کنید"
|
||
|
|
: "Click to retry sync"
|
||
|
|
}
|
||
|
|
className={cn(
|
||
|
|
"flex cursor-pointer items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors",
|
||
|
|
"disabled:cursor-not-allowed",
|
||
|
|
!isOnline
|
||
|
|
? "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300"
|
||
|
|
: isSyncing
|
||
|
|
? "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
|
||
|
|
: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{!isOnline ? (
|
||
|
|
<WifiOff className="h-3 w-3 shrink-0" aria-hidden />
|
||
|
|
) : isSyncing ? (
|
||
|
|
<RefreshCw className="h-3 w-3 shrink-0 animate-spin" aria-hidden />
|
||
|
|
) : (
|
||
|
|
<CloudUpload className="h-3 w-3 shrink-0" aria-hidden />
|
||
|
|
)}
|
||
|
|
<span>{label}</span>
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
}
|