Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

RepoListScreen.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

RepoListScreen.tsxBlame180 lines · 1 contributor
9a8fbedClaude1import React, { useCallback, useEffect, useState } from 'react';
2import {
3 FlatList,
4 StyleSheet,
5 Text,
6 TextInput,
7 View,
8} from 'react-native';
9import { SafeAreaView } from 'react-native-safe-area-context';
10import type { NativeStackScreenProps } from '@react-navigation/native-stack';
11import { useAuthStore } from '../store/authStore';
12import { listUserRepos } from '../api/repos';
13import type { Repository } from '../api/repos';
14import { RepoCard } from '../components/RepoCard';
15import { LoadingSpinner } from '../components/LoadingSpinner';
16import { ErrorBanner } from '../components/ErrorBanner';
17import { colors } from '../theme/colors';
18import type { RepoStackParamList } from '../navigation/AppNavigator';
19
20type Props = NativeStackScreenProps<RepoStackParamList, 'RepoList'>;
21
22export function RepoListScreen({ navigation }: Props): React.ReactElement {
23 const user = useAuthStore((s) => s.user);
24 const [repos, setRepos] = useState<Repository[]>([]);
25 const [filtered, setFiltered] = useState<Repository[]>([]);
26 const [query, setQuery] = useState('');
27 const [isLoading, setIsLoading] = useState(true);
28 const [error, setError] = useState<string | null>(null);
29 const [tick, setTick] = useState(0);
30
31 useEffect(() => {
32 if (!user) return;
33 let cancelled = false;
34 setIsLoading(true);
35 setError(null);
36
37 listUserRepos(user.username)
38 .then((data) => {
39 if (!cancelled) {
40 setRepos(data);
41 setFiltered(data);
42 }
43 })
44 .catch((err) => {
45 if (!cancelled) {
46 setError(err instanceof Error ? err.message : 'Failed to load repos');
47 }
48 })
49 .finally(() => {
50 if (!cancelled) setIsLoading(false);
51 });
52
53 return () => {
54 cancelled = true;
55 };
56 }, [user, tick]);
57
58 useEffect(() => {
59 const q = query.trim().toLowerCase();
60 if (!q) {
61 setFiltered(repos);
62 } else {
63 setFiltered(
64 repos.filter(
65 (r) =>
66 r.name.toLowerCase().includes(q) ||
67 (r.description ?? '').toLowerCase().includes(q),
68 ),
69 );
70 }
71 }, [query, repos]);
72
73 const retry = useCallback(() => setTick((t) => t + 1), []);
74
75 const handleRepoPress = useCallback(
76 (repo: Repository) => {
77 navigation.navigate('Repo', { owner: repo.ownerUsername, repo: repo.name });
78 },
79 [navigation],
80 );
81
82 const renderRepo = useCallback(
83 ({ item }: { item: Repository }) => (
84 <RepoCard repo={item} onPress={handleRepoPress} />
85 ),
86 [handleRepoPress],
87 );
88
89 const keyExtractor = useCallback((item: Repository) => String(item.id), []);
90
91 if (isLoading) return <LoadingSpinner fullScreen />;
92
93 return (
94 <SafeAreaView style={styles.safe} edges={['bottom']}>
95 <View style={styles.searchContainer}>
96 <TextInput
97 style={styles.searchInput}
98 value={query}
99 onChangeText={setQuery}
100 placeholder="Filter repositories..."
101 placeholderTextColor={colors.textMuted}
102 autoCapitalize="none"
103 autoCorrect={false}
104 clearButtonMode="while-editing"
105 />
106 </View>
107
108 {error !== null && <ErrorBanner message={error} onRetry={retry} />}
109
110 <FlatList
111 data={filtered}
112 keyExtractor={keyExtractor}
113 renderItem={renderRepo}
114 style={styles.list}
115 contentContainerStyle={styles.listContent}
116 ListEmptyComponent={
117 <View style={styles.emptyState}>
118 <Text style={styles.emptyText}>
119 {query ? 'No repositories match your filter.' : 'No repositories found.'}
120 </Text>
121 </View>
122 }
123 ListHeaderComponent={
124 <View style={styles.listHeader}>
125 <Text style={styles.listHeaderText}>
126 {filtered.length} {filtered.length === 1 ? 'repository' : 'repositories'}
127 </Text>
128 </View>
129 }
130 />
131 </SafeAreaView>
132 );
133}
134
135const styles = StyleSheet.create({
136 safe: {
137 flex: 1,
138 backgroundColor: colors.bg,
139 },
140 searchContainer: {
141 padding: 12,
142 backgroundColor: colors.bgSecondary,
143 borderBottomWidth: 1,
144 borderBottomColor: colors.border,
145 },
146 searchInput: {
147 backgroundColor: colors.bg,
148 borderWidth: 1,
149 borderColor: colors.border,
150 borderRadius: 8,
151 paddingHorizontal: 12,
152 paddingVertical: 10,
153 fontSize: 14,
154 color: colors.text,
155 },
156 list: {
157 flex: 1,
158 },
159 listContent: {
160 paddingTop: 8,
161 paddingBottom: 24,
162 },
163 listHeader: {
164 paddingHorizontal: 16,
165 paddingBottom: 8,
166 },
167 listHeaderText: {
168 fontSize: 12,
169 color: colors.textMuted,
170 },
171 emptyState: {
172 padding: 40,
173 alignItems: 'center',
174 },
175 emptyText: {
176 fontSize: 14,
177 color: colors.textMuted,
178 textAlign: 'center',
179 },
180});