Files
ERitors-Scribe-Desktop/components/offline/OfflineSyncManager.tsx
natreex d5b8191996 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.
2025-12-22 16:46:43 -05:00

233 lines
7.5 KiB
TypeScript

'use client';
import { useState, useEffect, useContext } from 'react';
import { SessionContext } from '@/context/SessionContext';
import { OfflineContext } from '@/context/OfflineContext';
import { useTranslations } from 'next-intl';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faCloudDownloadAlt,
faWifi,
faCheckCircle,
faSpinner,
faBook
} from '@fortawesome/free-solid-svg-icons';
interface SyncOption {
id: 'current' | 'recent' | 'all' | 'skip';
label: string;
description: string;
estimatedSize?: string;
bookCount?: number;
}
export default function OfflineSyncManager() {
const t = useTranslations();
const { session } = useContext(SessionContext);
const { syncBooks, isOnline } = useContext(OfflineContext);
const [showDialog, setShowDialog] = useState(false);
const [isFirstSync, setIsFirstSync] = useState(false);
const [syncProgress, setSyncProgress] = useState(0);
const [selectedOption, setSelectedOption] = useState<string>('');
useEffect(() => {
// Check if this is first time user sees sync options
const hasSeenSync = localStorage.getItem('hasSeenSyncDialog');
if (!hasSeenSync && isOnline && session?.user) {
setIsFirstSync(true);
setShowDialog(true);
}
}, [session, isOnline]);
const syncOptions: SyncOption[] = [
{
id: 'current',
label: t('sync.currentBook'),
description: t('sync.currentBookDesc'),
estimatedSize: '~5 MB',
bookCount: 1
},
{
id: 'recent',
label: t('sync.recentBooks'),
description: t('sync.recentBooksDesc'),
estimatedSize: '~25 MB',
bookCount: 3
},
{
id: 'all',
label: t('sync.allBooks'),
description: t('sync.allBooksDesc'),
estimatedSize: '~120 MB',
bookCount: 12
},
{
id: 'skip',
label: t('sync.skipForNow'),
description: t('sync.skipDesc'),
}
];
const handleSync = async (option: string) => {
if (option === 'skip') {
localStorage.setItem('hasSeenSyncDialog', 'true');
setShowDialog(false);
return;
}
setSyncProgress(0);
try {
// Simulate progressive download
const interval = setInterval(() => {
setSyncProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
return 100;
}
return prev + 10;
});
}, 200);
// Actual sync logic here
// await syncBooks(option);
localStorage.setItem('hasSeenSyncDialog', 'true');
localStorage.setItem('syncPreference', option);
setTimeout(() => {
setShowDialog(false);
}, 2000);
} catch (error) {
console.error('Sync failed:', error);
}
};
if (!showDialog) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-tertiary rounded-2xl p-8 max-w-2xl w-full mx-4 shadow-2xl">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<div className="relative">
<FontAwesomeIcon
icon={faCloudDownloadAlt}
className="w-8 h-8 text-primary"
/>
{isOnline && (
<FontAwesomeIcon
icon={faWifi}
className="w-4 h-4 text-success absolute -bottom-1 -right-1 bg-tertiary rounded-full p-0.5"
/>
)}
</div>
<div>
<h2 className="text-2xl font-bold">
{isFirstSync ? t('sync.welcomeOffline') : t('sync.syncOptions')}
</h2>
<p className="text-muted text-sm">
{t('sync.workAnywhere')}
</p>
</div>
</div>
{/* Sync Options */}
{syncProgress === 0 ? (
<div className="space-y-3 mb-6">
{syncOptions.map((option) => (
<button
key={option.id}
onClick={() => setSelectedOption(option.id)}
className={`
w-full p-4 rounded-lg border-2 text-left transition-all
${selectedOption === option.id
? 'border-primary bg-primary/10'
: 'border-gray-dark hover:border-gray'
}
`}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="font-semibold flex items-center gap-2">
{option.bookCount && (
<span className="text-primary">
<FontAwesomeIcon icon={faBook} className="w-4 h-4" />
{' '}{option.bookCount}
</span>
)}
{option.label}
</div>
<p className="text-sm text-muted mt-1">
{option.description}
</p>
</div>
{option.estimatedSize && (
<span className="text-sm text-muted">
{option.estimatedSize}
</span>
)}
</div>
</button>
))}
</div>
) : (
/* Progress Bar */
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-muted">
{t('sync.downloading')}...
</span>
<span className="text-sm font-semibold">
{syncProgress}%
</span>
</div>
<div className="h-2 bg-gray-dark rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-primary to-info transition-all duration-300"
style={{ width: `${syncProgress}%` }}
/>
</div>
{syncProgress === 100 && (
<div className="mt-4 text-center">
<FontAwesomeIcon
icon={faCheckCircle}
className="w-12 h-12 text-success mx-auto mb-2"
/>
<p className="text-success font-semibold">
{t('sync.complete')}!
</p>
</div>
)}
</div>
)}
{/* Actions */}
{syncProgress === 0 && (
<div className="flex gap-3 justify-end">
<button
onClick={() => handleSync('skip')}
className="px-6 py-2 text-muted hover:text-textPrimary transition-colors"
>
{t('common.later')}
</button>
<button
onClick={() => handleSync(selectedOption)}
disabled={!selectedOption}
className="
px-6 py-2 bg-primary hover:bg-primary/90
text-white rounded-lg transition-all
disabled:opacity-50 disabled:cursor-not-allowed
flex items-center gap-2
"
>
{selectedOption === 'skip' ? t('common.continue') : t('sync.startSync')}
{selectedOption && selectedOption !== 'skip' && (
<FontAwesomeIcon icon={faCloudDownloadAlt} className="w-4 h-4" />
)}
</button>
</div>
)}
</div>
</div>
);
}