270 lines
9.3 KiB
TypeScript
270 lines
9.3 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { nostrAuthService } from '@/lib/nostrAuth'
|
||
import { syncUserContentToCache, type SyncProgress } from '@/lib/userContentSync'
|
||
import { getLastSyncDate, setLastSyncDate as setLastSyncDateStorage, getCurrentTimestamp, calculateDaysBetween } from '@/lib/syncStorage'
|
||
import { MIN_EVENT_DATE } from '@/lib/platformConfig'
|
||
import { objectCache } from '@/lib/objectCache'
|
||
import { t } from '@/lib/i18n'
|
||
|
||
export function SyncProgressBar(): React.ReactElement | null {
|
||
console.warn('[SyncProgressBar] Component function called')
|
||
|
||
const [syncProgress, setSyncProgress] = useState<SyncProgress | null>(null)
|
||
const [isSyncing, setIsSyncing] = useState(false)
|
||
const [lastSyncDate, setLastSyncDate] = useState<number | null>(null)
|
||
const [totalDays, setTotalDays] = useState<number>(0)
|
||
const [isInitialized, setIsInitialized] = useState(false)
|
||
const [connectionState, setConnectionState] = useState<{ connected: boolean; pubkey: string | null }>({ connected: false, pubkey: null })
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
async function loadSyncStatus(): Promise<void> {
|
||
try {
|
||
const state = nostrAuthService.getState()
|
||
if (!state.connected || !state.pubkey) {
|
||
return
|
||
}
|
||
|
||
const storedLastSyncDate = await getLastSyncDate()
|
||
const currentTimestamp = getCurrentTimestamp()
|
||
const days = calculateDaysBetween(storedLastSyncDate, currentTimestamp)
|
||
|
||
setLastSyncDate(storedLastSyncDate)
|
||
setTotalDays(days)
|
||
} catch (loadError) {
|
||
console.error('Error loading sync status:', loadError)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
// Check connection state
|
||
const checkConnection = (): void => {
|
||
const state = nostrAuthService.getState()
|
||
console.warn('[SyncProgressBar] Initial connection check:', { connected: state.connected, pubkey: state.pubkey })
|
||
setConnectionState({ connected: state.connected ?? false, pubkey: state.pubkey ?? null })
|
||
setIsInitialized(true)
|
||
}
|
||
|
||
// Initial check
|
||
checkConnection()
|
||
|
||
// Listen to connection changes
|
||
const unsubscribe = nostrAuthService.subscribe((state) => {
|
||
console.warn('[SyncProgressBar] Connection state changed:', { connected: state.connected, pubkey: state.pubkey })
|
||
setConnectionState({ connected: state.connected ?? false, pubkey: state.pubkey ?? null })
|
||
})
|
||
|
||
return () => {
|
||
unsubscribe()
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
console.warn('[SyncProgressBar] Effect triggered:', { isInitialized, connected: connectionState.connected, pubkey: connectionState.pubkey, isSyncing })
|
||
|
||
if (!isInitialized) {
|
||
console.warn('[SyncProgressBar] Not initialized yet')
|
||
return
|
||
}
|
||
|
||
if (!connectionState.connected) {
|
||
console.warn('[SyncProgressBar] Not connected')
|
||
return
|
||
}
|
||
|
||
if (!connectionState.pubkey) {
|
||
console.warn('[SyncProgressBar] No pubkey')
|
||
return
|
||
}
|
||
|
||
void (async () => {
|
||
console.warn('[SyncProgressBar] Starting sync check...')
|
||
await loadSyncStatus()
|
||
|
||
// Auto-start sync if not recently synced
|
||
const storedLastSyncDate = await getLastSyncDate()
|
||
const currentTimestamp = getCurrentTimestamp()
|
||
const isRecentlySynced = storedLastSyncDate >= currentTimestamp - 3600
|
||
|
||
console.warn('[SyncProgressBar] Sync status:', { storedLastSyncDate, currentTimestamp, isRecentlySynced, isSyncing })
|
||
|
||
// Only auto-start if not recently synced
|
||
if (!isRecentlySynced && !isSyncing && connectionState.pubkey) {
|
||
console.warn('[SyncProgressBar] Starting auto-sync...')
|
||
setIsSyncing(true)
|
||
setSyncProgress({ currentStep: 0, totalSteps: 6, completed: false })
|
||
|
||
try {
|
||
await syncUserContentToCache(connectionState.pubkey, (progress) => {
|
||
setSyncProgress(progress)
|
||
if (progress.completed) {
|
||
setIsSyncing(false)
|
||
void loadSyncStatus()
|
||
}
|
||
})
|
||
// Check if sync completed successfully (if it didn't, isSyncing should still be false)
|
||
setIsSyncing(false)
|
||
} catch (autoSyncError) {
|
||
console.error('[SyncProgressBar] Error during auto-sync:', autoSyncError)
|
||
setIsSyncing(false)
|
||
setError(autoSyncError instanceof Error ? autoSyncError.message : 'Erreur de synchronisation')
|
||
}
|
||
} else {
|
||
console.warn('[SyncProgressBar] Skipping auto-sync:', { isRecentlySynced, isSyncing, hasPubkey: Boolean(connectionState.pubkey) })
|
||
}
|
||
})()
|
||
}, [isInitialized, connectionState.connected, connectionState.pubkey, isSyncing])
|
||
|
||
async function resynchronize(): Promise<void> {
|
||
try {
|
||
const state = nostrAuthService.getState()
|
||
if (!state.connected || !state.pubkey) {
|
||
return
|
||
}
|
||
|
||
setIsSyncing(true)
|
||
setSyncProgress({ currentStep: 0, totalSteps: 6, completed: false })
|
||
|
||
// Clear cache for user content (but keep other data)
|
||
await Promise.all([
|
||
objectCache.clear('author'),
|
||
objectCache.clear('series'),
|
||
objectCache.clear('publication'),
|
||
objectCache.clear('review'),
|
||
objectCache.clear('purchase'),
|
||
objectCache.clear('sponsoring'),
|
||
objectCache.clear('review_tip'),
|
||
])
|
||
|
||
// Reset last sync date to force full resync
|
||
await setLastSyncDateStorage(MIN_EVENT_DATE)
|
||
|
||
// Reload sync status
|
||
await loadSyncStatus()
|
||
|
||
// Start full resynchronization
|
||
if (state.pubkey !== null) {
|
||
await syncUserContentToCache(state.pubkey, (progress) => {
|
||
setSyncProgress(progress)
|
||
if (progress.completed) {
|
||
setIsSyncing(false)
|
||
void loadSyncStatus()
|
||
}
|
||
})
|
||
}
|
||
} catch (resyncError) {
|
||
console.error('Error resynchronizing:', resyncError)
|
||
setIsSyncing(false)
|
||
}
|
||
}
|
||
|
||
// Don't show if not initialized or not connected
|
||
if (!isInitialized || !connectionState.connected || !connectionState.pubkey) {
|
||
console.warn('[SyncProgressBar] Not rendering:', { isInitialized, connected: connectionState.connected, pubkey: connectionState.pubkey })
|
||
return null
|
||
}
|
||
|
||
console.warn('[SyncProgressBar] Rendering component')
|
||
|
||
// Check if sync is recently completed (within last hour)
|
||
const isRecentlySynced = lastSyncDate !== null && lastSyncDate >= getCurrentTimestamp() - 3600
|
||
|
||
const progressPercentage = syncProgress && syncProgress.totalSteps > 0
|
||
? Math.min(100, (syncProgress.currentStep / syncProgress.totalSteps) * 100)
|
||
: 0
|
||
|
||
const formatDate = (timestamp: number): string => {
|
||
const date = new Date(timestamp * 1000)
|
||
const locale = typeof window !== 'undefined' ? navigator.language : 'fr-FR'
|
||
return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||
}
|
||
|
||
const getStartDate = (): number => {
|
||
if (lastSyncDate !== null) {
|
||
return lastSyncDate
|
||
}
|
||
return MIN_EVENT_DATE
|
||
}
|
||
|
||
const startDate = getStartDate()
|
||
const endDate = getCurrentTimestamp()
|
||
|
||
return (
|
||
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-4 mt-6">
|
||
{error && (
|
||
<div className="mb-4 bg-red-900/30 border border-red-500/50 rounded p-3 text-red-300 text-sm">
|
||
{error}
|
||
<button
|
||
onClick={() => {
|
||
setError(null)
|
||
}}
|
||
className="ml-2 text-red-400 hover:text-red-200"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h3 className="text-lg font-semibold text-neon-cyan">
|
||
{t('settings.sync.title')}
|
||
</h3>
|
||
{!isSyncing && (
|
||
<button
|
||
onClick={() => {
|
||
void resynchronize()
|
||
}}
|
||
className="px-3 py-1 text-xs bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded border border-neon-cyan/50 hover:border-neon-cyan transition-colors"
|
||
>
|
||
{t('settings.sync.resync')}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{totalDays > 0 && (
|
||
<div className="mb-2">
|
||
<p className="text-sm text-cyber-accent">
|
||
{t('settings.sync.daysRange', {
|
||
startDate: formatDate(startDate),
|
||
endDate: formatDate(endDate),
|
||
days: totalDays,
|
||
})}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{isSyncing && syncProgress && (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-cyber-accent">
|
||
{t('settings.sync.progress', {
|
||
current: syncProgress.currentStep,
|
||
total: syncProgress.totalSteps,
|
||
})}
|
||
</span>
|
||
<span className="text-neon-cyan font-semibold">
|
||
{Math.round(progressPercentage)}%
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-cyber-dark rounded-full h-2 overflow-hidden">
|
||
<div
|
||
className="bg-neon-cyan h-full transition-all duration-300"
|
||
style={{ width: `${progressPercentage}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{!isSyncing && totalDays === 0 && isRecentlySynced && (
|
||
<p className="text-sm text-green-400">
|
||
{t('settings.sync.completed')}
|
||
</p>
|
||
)}
|
||
|
||
{!isSyncing && totalDays === 0 && !isRecentlySynced && (
|
||
<p className="text-sm text-cyber-accent">
|
||
{t('settings.sync.ready')}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|