Files
carry-your-live/carry-your-live/src/components/SyncStatus.tsx
T
2026-08-23 11:36:17 +02:00

196 lines
5.8 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useSettings } from '@/theme';
import Svg, { Path, Circle } from 'react-native-svg';
import { runSyncGuarded, getLastSyncTime } from '@/database/sync';
interface SyncStatusProps {
compact?: boolean;
}
export default function SyncStatus({ compact = false }: SyncStatusProps) {
const { theme } = useSettings();
const [status, setStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle');
const [lastSync, setLastSync] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [now] = useState(() => Date.now());
const loadLastSync = async () => {
const time = await getLastSyncTime();
setLastSync(time);
};
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadLastSync();
}, []);
const handleSync = async () => {
setStatus('syncing');
setError(null);
try {
await runSyncGuarded();
setStatus('success');
await loadLastSync();
setTimeout(() => setStatus('idle'), 3000);
} catch (err) {
setStatus('error');
setError(err instanceof Error ? err.message : 'Sync failed');
setTimeout(() => setStatus('idle'), 5000);
}
};
const formatTime = (timestamp: number | null): string => {
if (!timestamp) return 'Never';
const diff = now - timestamp;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return new Date(timestamp).toLocaleDateString();
};
const getStatusIcon = () => {
switch (status) {
case 'syncing':
return (
<Svg width={16} height={16} viewBox="0 0 24 24">
<Path
d="M12 2a10 10 0 1 0 10 10"
stroke={theme.accent}
strokeWidth={2}
strokeLinecap="round"
fill="none"
/>
</Svg>
);
case 'success':
return (
<Svg width={16} height={16} viewBox="0 0 24 24">
<Path d="M5 12l5 5 9-10" stroke={theme.success} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
</Svg>
);
case 'error':
return (
<Svg width={16} height={16} viewBox="0 0 24 24">
<Circle cx={12} cy={12} r={10} stroke={theme.danger} strokeWidth={2} fill="none" />
<Path d="M12 8v4M12 16h.01" stroke={theme.danger} strokeWidth={2} strokeLinecap="round" />
</Svg>
);
default:
return (
<Svg width={16} height={16} viewBox="0 0 24 24">
<Path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" stroke={theme.textFaint} strokeWidth={1.8} fill="none" />
<Path d="M12 22V12" stroke={theme.textFaint} strokeWidth={1.8} strokeLinecap="round" />
</Svg>
);
}
};
const getStatusText = () => {
switch (status) {
case 'syncing': return 'Syncing...';
case 'success': return 'Synced';
case 'error': return 'Sync failed';
default: return lastSync ? `Last sync: ${formatTime(lastSync)}` : 'Not synced';
}
};
if (compact) {
return (
<TouchableOpacity style={[styles.compact, { backgroundColor: theme.sheetBg }]} onPress={handleSync} disabled={status === 'syncing'} activeOpacity={0.7}>
<View style={styles.compactRow}>
{getStatusIcon()}
<Text style={[styles.compactText, { color: theme.textMuted }]}>
{status === 'syncing' ? 'Syncing...' : formatTime(lastSync)}
</Text>
</View>
</TouchableOpacity>
);
}
return (
<View style={[styles.container, { backgroundColor: theme.sheetBg }]}>
<View style={styles.header}>
<View style={styles.statusRow}>
{getStatusIcon()}
<Text style={[styles.statusText, { color: theme.text }]}>{getStatusText()}</Text>
</View>
<TouchableOpacity
style={[styles.syncButton, { backgroundColor: status === 'syncing' ? theme.accentSoft : theme.accent }]}
onPress={handleSync}
disabled={status === 'syncing'}
activeOpacity={0.8}
>
<Text style={[styles.syncButtonText, { color: status === 'syncing' ? theme.accent : theme.accentText }]}>
{status === 'syncing' ? 'Syncing...' : 'Sync Now'}
</Text>
</TouchableOpacity>
</View>
{error && (
<View style={styles.error}>
<Text style={[styles.errorText, { color: theme.dangerStrong }]}>{error}</Text>
</View>
)}
<Text style={[styles.lastSync, { color: theme.textFaint }]}>
Last sync: {formatTime(lastSync)}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
borderRadius: 12,
marginVertical: 8,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statusText: {
fontSize: 14,
fontWeight: '500',
},
syncButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
},
syncButtonText: {
fontSize: 13,
fontWeight: '600',
},
error: {
paddingTop: 8,
borderTopWidth: 1,
},
errorText: {
fontSize: 12,
},
lastSync: {
fontSize: 11,
marginTop: 8,
},
compact: {
paddingVertical: 6,
paddingHorizontal: 10,
borderRadius: 8,
borderWidth: 1,
},
compactRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
compactText: {
fontSize: 12,
},
});