126 lines
3.7 KiB
TypeScript
126 lines
3.7 KiB
TypeScript
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|||
|
|
import {
|
|||
|
|
ActivityIndicator,
|
|||
|
|
FlatList,
|
|||
|
|
RefreshControl,
|
|||
|
|
StyleSheet,
|
|||
|
|
Text,
|
|||
|
|
TouchableOpacity,
|
|||
|
|
View,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import * as api from '../api/endpoints';
|
|||
|
|
import { Project } from '../api/types';
|
|||
|
|
import { useSession } from '../auth/session';
|
|||
|
|
import { getCursor, getProjects, saveProjectList } from '../db/repositories';
|
|||
|
|
import { isOnline } from '../net/connectivity';
|
|||
|
|
import { initialPull, runSync } from '../sync/engine';
|
|||
|
|
import { RootStackParamList } from '../navigation/types';
|
|||
|
|
|
|||
|
|
type Props = NativeStackScreenProps<RootStackParamList, 'Projects'>;
|
|||
|
|
|
|||
|
|
function normalize(res: { data?: Project[] } | Project[]): Project[] {
|
|||
|
|
return Array.isArray(res) ? res : res.data ?? [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function ProjectsScreen({ navigation }: Props) {
|
|||
|
|
const { user, signOut } = useSession();
|
|||
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [opening, setOpening] = useState<number | null>(null);
|
|||
|
|
|
|||
|
|
const load = useCallback(async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
if (await isOnline()) {
|
|||
|
|
const list = normalize(await api.listProjects());
|
|||
|
|
await saveProjectList(list);
|
|||
|
|
}
|
|||
|
|
setProjects(await getProjects());
|
|||
|
|
} catch {
|
|||
|
|
setProjects(await getProjects()); // fallback offline
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
void load();
|
|||
|
|
}, [load]);
|
|||
|
|
|
|||
|
|
const openProject = useCallback(
|
|||
|
|
async (p: Project) => {
|
|||
|
|
setOpening(p.id);
|
|||
|
|
try {
|
|||
|
|
const online = await isOnline();
|
|||
|
|
if (online) {
|
|||
|
|
const cursor = await getCursor(p.id);
|
|||
|
|
if (cursor) await runSync(p.id);
|
|||
|
|
else await initialPull(p.id);
|
|||
|
|
}
|
|||
|
|
navigation.navigate('ProjectDetail', { projectId: p.id, name: p.name });
|
|||
|
|
} finally {
|
|||
|
|
setOpening(null);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
[navigation],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={styles.container}>
|
|||
|
|
<View style={styles.header}>
|
|||
|
|
<Text style={styles.hello}>Hola, {user?.name ?? ''}</Text>
|
|||
|
|
<TouchableOpacity onPress={signOut}>
|
|||
|
|
<Text style={styles.logout}>Salir</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<FlatList
|
|||
|
|
data={projects}
|
|||
|
|
keyExtractor={(p) => String(p.id)}
|
|||
|
|
refreshControl={<RefreshControl refreshing={loading} onRefresh={load} />}
|
|||
|
|
ListEmptyComponent={
|
|||
|
|
loading ? null : <Text style={styles.empty}>No hay proyectos.</Text>
|
|||
|
|
}
|
|||
|
|
renderItem={({ item }) => (
|
|||
|
|
<TouchableOpacity style={styles.row} onPress={() => openProject(item)}>
|
|||
|
|
<View style={{ flex: 1 }}>
|
|||
|
|
<Text style={styles.name}>{item.name}</Text>
|
|||
|
|
{item.reference ? <Text style={styles.ref}>{item.reference}</Text> : null}
|
|||
|
|
</View>
|
|||
|
|
{opening === item.id ? (
|
|||
|
|
<ActivityIndicator />
|
|||
|
|
) : (
|
|||
|
|
<Text style={styles.chevron}>›</Text>
|
|||
|
|
)}
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
)}
|
|||
|
|
/>
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
container: { flex: 1 },
|
|||
|
|
header: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
padding: 16,
|
|||
|
|
},
|
|||
|
|
hello: { fontSize: 16, fontWeight: '600' },
|
|||
|
|
logout: { color: '#b00020', fontWeight: '600' },
|
|||
|
|
row: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
paddingHorizontal: 16,
|
|||
|
|
paddingVertical: 14,
|
|||
|
|
borderTopWidth: StyleSheet.hairlineWidth,
|
|||
|
|
borderColor: '#ddd',
|
|||
|
|
},
|
|||
|
|
name: { fontSize: 16, fontWeight: '600' },
|
|||
|
|
ref: { fontSize: 13, color: '#666', marginTop: 2 },
|
|||
|
|
chevron: { fontSize: 24, color: '#999' },
|
|||
|
|
empty: { textAlign: 'center', marginTop: 40, color: '#888' },
|
|||
|
|
});
|