import { useState, useEffect, useRef, useCallback } from 'react'; import { addScannedCardToOwned, addScannedCardToCollection, addScannedCardToDeck, } from './scanner-route-api.js'; const OFFLINE_QUEUE_KEY = 'deckhearth:offline-scan-queue'; const MAX_RETRIES = 3; function loadQueue() { if (typeof window === 'undefined') return []; try { return JSON.parse(localStorage.getItem(OFFLINE_QUEUE_KEY)) || []; } catch { return []; } } function persistQueue(queue) { if (typeof window === 'undefined') return; localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue)); } async function executeAction(action, payload) { switch (action) { case 'addToOwned': return addScannedCardToOwned(payload); case 'addToCollection': return addScannedCardToCollection(payload, payload.collectionId); case 'addToDeck': return addScannedCardToDeck(payload, payload.deckId); default: throw new Error(`Unknown offline action: ${action}`); } } export function useScannerOffline() { const [isOnline, setIsOnline] = useState( () => typeof navigator !== 'undefined' ? navigator.onLine : true ); const [queuedActions, setQueuedActions] = useState(loadQueue); const processingRef = useRef(false); const updateQueue = useCallback((updater) => { setQueuedActions((prev) => { const next = typeof updater === 'function' ? updater(prev) : updater; persistQueue(next); return next; }); }, []); const processQueue = useCallback(async () => { if (processingRef.current) return; processingRef.current = true; try { const current = loadQueue(); const pending = current.filter((item) => item.status === 'pending'); for (const item of pending) { try { await executeAction(item.action, item.payload); updateQueue((prev) => prev.filter((q) => q.id !== item.id)); } catch { updateQueue((prev) => prev.map((q) => { if (q.id !== item.id) return q; const retries = q.retries + 1; return { ...q, retries, status: retries >= MAX_RETRIES ? 'failed' : 'pending', }; }) ); } } } finally { processingRef.current = false; } }, [updateQueue]); useEffect(() => { const goOnline = () => { setIsOnline(true); processQueue(); }; const goOffline = () => setIsOnline(false); window.addEventListener('online', goOnline); window.addEventListener('offline', goOffline); return () => { window.removeEventListener('online', goOnline); window.removeEventListener('offline', goOffline); }; }, [processQueue]); // Process any items left over from a previous session on mount useEffect(() => { if (isOnline && loadQueue().some((q) => q.status === 'pending')) { processQueue(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const enqueue = useCallback( async (action, payload) => { const item = { id: Date.now() + Math.random(), action, payload, timestamp: Date.now(), retries: 0, status: 'pending', }; if (isOnline) { try { await executeAction(action, payload); return; } catch { // Network error while supposedly online — fall through to queue } } updateQueue((prev) => [...prev, item]); }, [isOnline, updateQueue] ); const pendingCount = queuedActions.filter((q) => q.status === 'pending').length; return { isOnline, queuedActions, enqueue, processQueue, pendingCount }; }