Add offline mode components and enhance synchronization logic

- Introduced `OfflineSyncManager`, `OfflineToggle`, `OfflinePinSetup`, `OfflineIndicator`, and `OfflineSyncDetails` components to support offline mode functionality.
- Added PIN verification (`OfflinePinVerify`) for secure offline access.
- Implemented sync options for books (current, recent, all) with progress tracking and improved user feedback.
- Enhanced offline context integration and error handling in sync processes.
- Refined UI elements for better online/offline status visibility and manual sync controls.
This commit is contained in:
natreex
2025-12-22 16:46:43 -05:00
parent 515d469ba7
commit d5b8191996
6 changed files with 912 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
'use client';
import React, { useContext } from 'react';
import OfflineContext from '@/context/OfflineContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faWifi, faWifiSlash, faSync, faCircle, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons';
/**
* OfflineIndicator - Displays current online/offline status and sync progress
* Allows user to toggle manual offline mode
*/
export default function OfflineIndicator() {
const { offlineMode, toggleOfflineMode, syncNow } = useContext(OfflineContext);
const {
isOffline,
isManuallyOffline,
isNetworkOnline,
isDatabaseInitialized,
syncProgress,
lastSyncAt,
error
} = offlineMode;
// Determine status color and text
const getStatusInfo = () => {
if (!isDatabaseInitialized) {
return {
color: 'text-gray-400',
bgColor: 'bg-gray-100',
icon: faCircle,
text: 'DB non initialisée'
};
}
if (error) {
return {
color: 'text-red-500',
bgColor: 'bg-red-50',
icon: faExclamationTriangle,
text: 'Erreur de sync'
};
}
if (syncProgress.isSyncing) {
return {
color: 'text-blue-500',
bgColor: 'bg-blue-50',
icon: faSync,
text: 'Synchronisation...',
spin: true
};
}
if (isOffline) {
if (syncProgress.pendingChanges > 0) {
return {
color: 'text-orange-500',
bgColor: 'bg-orange-50',
icon: faWifiSlash,
text: `Hors ligne (${syncProgress.pendingChanges} en attente)`
};
}
return {
color: 'text-orange-500',
bgColor: 'bg-orange-50',
icon: faWifiSlash,
text: isManuallyOffline ? 'Mode hors ligne' : 'Hors ligne'
};
}
if (syncProgress.pendingChanges > 0) {
return {
color: 'text-yellow-500',
bgColor: 'bg-yellow-50',
icon: faSync,
text: `${syncProgress.pendingChanges} changements à sync`
};
}
return {
color: 'text-green-500',
bgColor: 'bg-green-50',
icon: faCheck,
text: 'Synchronisé'
};
};
const statusInfo = getStatusInfo();
// Format last sync time
const formatLastSync = () => {
if (!lastSyncAt) return 'Jamais';
const diff = Date.now() - lastSyncAt;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(minutes / 60);
if (hours > 0) return `Il y a ${hours}h`;
if (minutes > 0) return `Il y a ${minutes}min`;
return 'À l\'instant';
};
const handleSyncNow = async () => {
if (!isOffline && !syncProgress.isSyncing) {
await syncNow();
}
};
return (
<div className="flex items-center gap-3 px-4 py-2 bg-white border-b border-gray-200">
{/* Status indicator */}
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${statusInfo.bgColor}`}>
<FontAwesomeIcon
icon={statusInfo.icon}
className={`${statusInfo.color} text-sm`}
spin={statusInfo.spin}
/>
<span className={`text-sm font-medium ${statusInfo.color}`}>
{statusInfo.text}
</span>
</div>
{/* Last sync time */}
{isDatabaseInitialized && !isOffline && (
<span className="text-xs text-gray-500">
Dernière sync: {formatLastSync()}
</span>
)}
{/* Error message */}
{error && (
<span className="text-xs text-red-500 max-w-xs truncate" title={error}>
{error}
</span>
)}
{/* Actions */}
<div className="ml-auto flex items-center gap-2">
{/* Manual sync button */}
{isDatabaseInitialized && !isOffline && !syncProgress.isSyncing && (
<button
onClick={handleSyncNow}
className="px-3 py-1 text-sm text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="Synchroniser maintenant"
>
<FontAwesomeIcon icon={faSync} className="mr-1" />
Sync
</button>
)}
{/* Offline toggle */}
{isDatabaseInitialized && (
<button
onClick={toggleOfflineMode}
className={`px-3 py-1 text-sm rounded transition-colors ${
isManuallyOffline
? 'bg-orange-100 text-orange-700 hover:bg-orange-200'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
title={isManuallyOffline ? 'Passer en ligne' : 'Passer hors ligne'}
disabled={!isNetworkOnline && !isManuallyOffline}
>
<FontAwesomeIcon
icon={isOffline ? faWifiSlash : faWifi}
className="mr-1"
/>
{isManuallyOffline ? 'En ligne' : 'Hors ligne'}
</button>
)}
</div>
</div>
);
}