story-research-zapwall/components/SyncProgressBar.tsx
2026-01-10 09:41:57 +01:00

347 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react'
import { nostrAuthService } from '@/lib/nostrAuth'
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'
import { useSyncProgress } from '@/lib/hooks/useSyncProgress'
export function SyncProgressBar(): React.ReactElement | null {
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)
const loadSyncStatus = useCallback(async (): 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)
}
}, [])
const { syncProgress, isSyncing, startMonitoring, stopMonitoring } = useSyncProgress({
onComplete: loadSyncStatus,
})
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 runAutoSyncCheck({
connection: { connected: connectionState.connected, pubkey: connectionState.pubkey },
isSyncing,
loadSyncStatus,
startMonitoring,
stopMonitoring,
setError,
})
}, [isInitialized, connectionState.connected, connectionState.pubkey, isSyncing, loadSyncStatus, startMonitoring, stopMonitoring])
async function resynchronize(): Promise<void> {
try {
const state = nostrAuthService.getState()
if (!state.connected || !state.pubkey) {
return
}
// 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 via Service Worker
if (state.pubkey !== null) {
const { swClient } = await import('@/lib/swClient')
const isReady = await swClient.isReady()
if (isReady) {
await swClient.startUserSync(state.pubkey)
startMonitoring()
} else {
stopMonitoring()
}
}
} catch (resyncError) {
console.error('Error resynchronizing:', resyncError)
stopMonitoring()
}
}
// Don't show if not initialized or not connected
if (!isInitialized || !connectionState.connected || !connectionState.pubkey) {
return null
}
// Check if sync is recently completed (within last hour)
const isRecentlySynced = lastSyncDate !== null && lastSyncDate >= getCurrentTimestamp() - 3600
const progressPercentage = computeProgressPercentage(syncProgress)
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">
<SyncErrorBanner
error={error}
onDismiss={() => {
setError(null)
}}
/>
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-neon-cyan">
{t('settings.sync.title')}
</h3>
<SyncResyncButton
isSyncing={isSyncing}
onClick={() => {
void resynchronize()
}}
/>
</div>
<SyncDateRange
totalDays={totalDays}
startDate={formatDate(startDate)}
endDate={formatDate(endDate)}
/>
<SyncProgressSection
isSyncing={isSyncing}
syncProgress={syncProgress}
progressPercentage={progressPercentage}
/>
<SyncStatusMessage
isSyncing={isSyncing}
totalDays={totalDays}
isRecentlySynced={isRecentlySynced}
/>
</div>
)
}
function computeProgressPercentage(syncProgress: ReturnType<typeof useSyncProgress>['syncProgress']): number {
if (!syncProgress || syncProgress.totalSteps <= 0) {
return 0
}
return Math.min(100, (syncProgress.currentStep / syncProgress.totalSteps) * 100)
}
function SyncErrorBanner(params: { error: string | null; onDismiss: () => void }): React.ReactElement | null {
if (!params.error) {
return null
}
return (
<div className="mb-4 bg-red-900/30 border border-red-500/50 rounded p-3 text-red-300 text-sm">
{params.error}
<button onClick={params.onDismiss} className="ml-2 text-red-400 hover:text-red-200">
×
</button>
</div>
)
}
function SyncResyncButton(params: { isSyncing: boolean; onClick: () => void }): React.ReactElement | null {
if (params.isSyncing) {
return null
}
return (
<button
onClick={params.onClick}
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>
)
}
function SyncDateRange(params: { totalDays: number; startDate: string; endDate: string }): React.ReactElement | null {
if (params.totalDays <= 0) {
return null
}
return (
<div className="mb-2">
<p className="text-sm text-cyber-accent">
{t('settings.sync.daysRange', {
startDate: params.startDate,
endDate: params.endDate,
days: params.totalDays,
})}
</p>
</div>
)
}
function SyncProgressSection(params: {
isSyncing: boolean
syncProgress: ReturnType<typeof useSyncProgress>['syncProgress']
progressPercentage: number
}): React.ReactElement | null {
if (!params.isSyncing || !params.syncProgress) {
return null
}
return (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-cyber-accent">
{t('settings.sync.progress', {
current: params.syncProgress.currentStep,
total: params.syncProgress.totalSteps,
})}
</span>
<span className="text-neon-cyan font-semibold">{Math.round(params.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: `${params.progressPercentage}%` }} />
</div>
</div>
)
}
function SyncStatusMessage(params: { isSyncing: boolean; totalDays: number; isRecentlySynced: boolean }): React.ReactElement | null {
if (params.isSyncing || params.totalDays !== 0) {
return null
}
if (params.isRecentlySynced) {
return <p className="text-sm text-green-400">{t('settings.sync.completed')}</p>
}
return <p className="text-sm text-cyber-accent">{t('settings.sync.ready')}</p>
}
async function runAutoSyncCheck(params: {
connection: { connected: boolean; pubkey: string | null }
isSyncing: boolean
loadSyncStatus: () => Promise<void>
startMonitoring: () => void
stopMonitoring: () => void
setError: (value: string | null) => void
}): Promise<void> {
console.warn('[SyncProgressBar] Starting sync check...')
await params.loadSyncStatus()
const shouldStart = await shouldAutoStartSync({
isSyncing: params.isSyncing,
pubkey: params.connection.pubkey,
})
if (!shouldStart || !params.connection.pubkey) {
console.warn('[SyncProgressBar] Skipping auto-sync:', { shouldStart, isSyncing: params.isSyncing, hasPubkey: Boolean(params.connection.pubkey) })
return
}
console.warn('[SyncProgressBar] Starting auto-sync...')
await startAutoSync({
pubkey: params.connection.pubkey,
startMonitoring: params.startMonitoring,
stopMonitoring: params.stopMonitoring,
setError: params.setError,
})
}
async function shouldAutoStartSync(params: { isSyncing: boolean; pubkey: string | null }): Promise<boolean> {
if (params.isSyncing || !params.pubkey) {
return false
}
const storedLastSyncDate = await getLastSyncDate()
const currentTimestamp = getCurrentTimestamp()
const isRecentlySynced = storedLastSyncDate >= currentTimestamp - 3600
console.warn('[SyncProgressBar] Sync status:', { storedLastSyncDate, currentTimestamp, isRecentlySynced })
return !isRecentlySynced
}
async function startAutoSync(params: {
pubkey: string
startMonitoring: () => void
stopMonitoring: () => void
setError: (value: string | null) => void
}): Promise<void> {
try {
const { swClient } = await import('@/lib/swClient')
const isReady = await swClient.isReady()
if (!isReady) {
params.stopMonitoring()
return
}
await swClient.startUserSync(params.pubkey)
params.startMonitoring()
} catch (autoSyncError) {
console.error('[SyncProgressBar] Error during auto-sync:', autoSyncError)
params.stopMonitoring()
params.setError(autoSyncError instanceof Error ? autoSyncError.message : 'Erreur de synchronisation')
}
}