Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/app/appStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,6 @@ export const appStyles = StyleSheet.create({
top: 3,
right: 4,
},
syncButtonContainer: {
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
refreshingIndicator: {
flexDirection: 'row',
alignItems: 'center',
Expand Down
14 changes: 6 additions & 8 deletions src/app/tabs/ProjectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import {
import { logger } from '../../utils/logger';
import { Project } from '../../types/db/types';
import { getProjects } from '../../db/queries';
import { appStyles as styles } from '../appStyles';
import { useNavigation } from '@react-navigation/native';
import { SyncButton } from '../../components/ui/SyncButton';
import FluentLogo from '../../assets/icons/fluent-logo.svg';
import { RootStackParamList } from '../../types/navigation/types';
import { StackNavigationProp } from '@react-navigation/stack';
import { Ionicons } from '@react-native-vector-icons/ionicons';
import { appStyles as styles } from '../appStyles';
import { SyncButton } from '../../components/ui/SyncButton';
import { RootStackParamList } from '../../types/navigation/types';

const log = logger.create('ProjectListScreen');
const log = logger.create('ProjectList');
type Nav = StackNavigationProp<RootStackParamList, 'Projects'>;

export default function ProjectsScreen() {
export default function ProjectList() {
const navigation = useNavigation<Nav>();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -65,9 +65,7 @@ export default function ProjectsScreen() {
<FluentLogo width={160} height={54} />
</View>

<View style={styles.syncButtonContainer}>
<SyncButton onSyncComplete={handleSyncComplete} />
</View>
<SyncButton onSyncComplete={handleSyncComplete} />

<View style={styles.sectionHeader}>
<Ionicons name="folder-outline" size={24} color="#000" />
Expand Down
4 changes: 2 additions & 2 deletions src/app/tabs/ViewChapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ChapterAssignmentData, VerseData } from '../../types/db/types';
import { getChapterAssignmentById, getBibleTexts } from '../../db/queries';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';

const log = logger.create('ViewChapterScreen');
const log = logger.create('ViewChapter');

if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental?.(true);
Expand All @@ -26,7 +26,7 @@ if (Platform.OS === 'android') {
type Route = RouteProp<RootStackParamList, 'VerseDetail'>;
type VerseState = 'idle' | 'recording' | 'recorded';

export default function VerseDetailScreen() {
export default function ViewChapter() {
const navigation = useNavigation();
const { chapterId, chapterName, language, projectName } =
useRoute<Route>().params;
Expand Down
4 changes: 2 additions & 2 deletions src/app/tabs/ViewProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import { appStyles as styles } from '../appStyles';
import { RootStackParamList } from '../../types/navigation/types';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';

const log = logger.create('ChaptersScreen');
const log = logger.create('ViewProject');

type Nav = StackNavigationProp<RootStackParamList, 'Chapters'>;
type Route = RouteProp<RootStackParamList, 'Chapters'>;

export default function ChaptersScreen() {
export default function ViewProject() {
const navigation = useNavigation<Nav>();
const { projectId, projectName, language } = useRoute<Route>().params;

Expand Down
182 changes: 159 additions & 23 deletions src/components/ui/SyncButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import {
TouchableOpacity,
Text,
Expand All @@ -8,54 +8,191 @@ import {
StyleProp,
ViewStyle,
} from 'react-native';
import { getUserEmailSync } from '../../services/storage';
import { Ionicons } from '@react-native-vector-icons/ionicons';
import { syncAllData } from '../../services/sync';
import {
getSyncState,
getSyncError,
KV_KEYS,
getUserEmailSync,
} from '../../services/storage';
import { logger } from '../../utils/logger';

const log = logger.create('SyncButton');

type SyncStateType = 'normal' | 'syncing' | 'never' | 'error';

interface SyncButtonProps {
onSyncComplete?: () => void;
style?: StyleProp<ViewStyle>;
onSyncStart?: () => void;
}

export function SyncButton({ onSyncComplete, style }: SyncButtonProps) {
export function SyncButton({
onSyncComplete,
style,
onSyncStart,
}: SyncButtonProps) {
const [stateType, setStateType] = useState<SyncStateType>('normal');
const [displayText, setDisplayText] = useState('');
const [isSyncing, setIsSyncing] = useState(false);

const getRelativeTime = (isoTimestamp: string | undefined): string => {
if (!isoTimestamp) return 'Never synced';

const now = new Date();
const syncTime = new Date(isoTimestamp);
const diffMs = now.getTime() - syncTime.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);

if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? 's' : ''} ago`;
if (diffHours < 24)
return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
};

const getFailedStep = (): string | null => {
const userError = getSyncError(KV_KEYS.SYNC_ERROR_USER);
if (userError) return 'user';

const masterDataError = getSyncError(KV_KEYS.SYNC_ERROR_MASTER_DATA);
if (masterDataError) return 'master data';

const projectsError = getSyncError(KV_KEYS.SYNC_ERROR_PROJECTS);
if (projectsError) return 'projects';

const chaptersError = getSyncError(KV_KEYS.SYNC_ERROR_CHAPTER_ASSIGNMENTS);
if (chaptersError) return 'chapter assignments';

const projectUnitsError = getSyncError(KV_KEYS.SYNC_ERROR_PROJECT_UNITS);
if (projectUnitsError) return 'project units';

const bibleTextsError = getSyncError(KV_KEYS.SYNC_ERROR_BIBLE_TEXTS);
if (bibleTextsError) return 'bible texts';

return null;
};

const updateState = useCallback(() => {
if (isSyncing) {
setStateType('syncing');
setDisplayText('Syncing...');
} else {
const failedStep = getFailedStep();
const syncState = getSyncState();

if (failedStep) {
setStateType('error');
setDisplayText(`Sync failed: ${failedStep}`);
} else if (!syncState.lastSyncedAt) {
setStateType('never');
setDisplayText('Never synced');
} else {
setStateType('normal');
setDisplayText(
`Last synced: ${getRelativeTime(syncState.lastSyncedAt)}`,
);
}
}
}, [isSyncing]);

useEffect(() => {
updateState();
}, [isSyncing, updateState]);

useEffect(() => {
if (stateType === 'normal') {
const interval = setInterval(() => {
updateState();
}, 60000);

return () => clearInterval(interval);
}
}, [stateType, updateState]);

const handleSync = useCallback(async () => {
try {
setIsSyncing(true);
onSyncStart?.();
const email = getUserEmailSync();

if (!email) {
log.error('No user email found for sync');
setIsSyncing(false);
return;
}

log.info('Triggering full sync...');
log.info('Triggering sync...');
await syncAllData(email);

log.info('Sync completed successfully');
updateState();
onSyncComplete?.();
} catch (error) {
log.error('Sync failed', { error });
updateState();
} finally {
setIsSyncing(false);
}
}, [onSyncComplete]);
}, [onSyncStart, onSyncComplete, updateState]);

const getStateColors = () => {
switch (stateType) {
case 'syncing':
return {
backgroundColor: '#e6f1fb',
borderColor: '#b5d4f4',
textColor: '#1a6ef5',
};
case 'error':
return {
backgroundColor: '#fcebeb',
borderColor: '#f7c1c1',
textColor: '#d32f2f',
};
case 'never':
case 'normal':
default:
return {
backgroundColor: '#fff',
borderColor: '#e0e0e0',
textColor: '#999',
};
}
};

const colors = getStateColors();

return (
<View style={[styles.container, style]}>
<View
style={[
styles.container,
style,
{
backgroundColor: colors.backgroundColor,
borderColor: colors.borderColor,
},
]}
>
<View style={styles.content}>
<Text style={[styles.text, { color: colors.textColor }]}>
{displayText}
</Text>
</View>

<TouchableOpacity
style={[styles.button, isSyncing && styles.buttonDisabled]}
onPress={handleSync}
disabled={isSyncing}
activeOpacity={0.7}
style={styles.syncButton}
>
{isSyncing ? (
<ActivityIndicator size="small" color="#fff" />
<ActivityIndicator size="small" color={colors.textColor} />
) : (
<Text style={styles.buttonText}>Sync</Text>
<Ionicons name="refresh" size={20} color={colors.textColor} />
)}
</TouchableOpacity>
</View>
Expand All @@ -64,24 +201,23 @@ export function SyncButton({ onSyncComplete, style }: SyncButtonProps) {

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
button: {
backgroundColor: '#1a6ef5',
paddingHorizontal: 24,
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 8,
minWidth: 100,
justifyContent: 'center',
alignItems: 'center',
borderBottomWidth: 1,
marginBottom: 8,
},
buttonDisabled: {
opacity: 0.6,
content: {
flex: 1,
},
buttonText: {
color: '#fff',
text: {
fontSize: 14,
fontWeight: '600',
fontWeight: '500',
},
syncButton: {
padding: 8,
marginLeft: 12,
},
});
Loading
Loading