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

RepoScreen.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.

RepoScreen.tsxBlame337 lines · 1 contributor
9a8fbedClaude1import React, { useCallback } from 'react';
2import {
3 FlatList,
4 ScrollView,
5 StyleSheet,
6 Text,
7 TouchableOpacity,
8 View,
9} from 'react-native';
10import { SafeAreaView } from 'react-native-safe-area-context';
11import type { NativeStackScreenProps } from '@react-navigation/native-stack';
12import { useRepo, useTree } from '../hooks/useRepo';
13import { LoadingSpinner } from '../components/LoadingSpinner';
14import { ErrorBanner } from '../components/ErrorBanner';
15import { colors } from '../theme/colors';
16import type { RepoStackParamList } from '../navigation/AppNavigator';
17import type { TreeEntry } from '../api/repos';
18
19type Props = NativeStackScreenProps<RepoStackParamList, 'Repo'>;
20
21function FileRow({
22 entry,
23 onPress,
24}: {
25 entry: TreeEntry;
26 onPress: (entry: TreeEntry) => void;
27}): React.ReactElement {
28 const handlePress = useCallback(() => onPress(entry), [entry, onPress]);
29 const icon = entry.type === 'tree' ? '📁' : '📄';
30
31 return (
32 <TouchableOpacity style={styles.fileRow} onPress={handlePress} activeOpacity={0.75}>
33 <Text style={styles.fileIcon}>{icon}</Text>
34 <Text style={styles.fileName} numberOfLines={1}>
35 {entry.name}
36 </Text>
37 {entry.type === 'tree' && <Text style={styles.chevron}>›</Text>}
38 </TouchableOpacity>
39 );
40}
41
42export function RepoScreen({ route, navigation }: Props): React.ReactElement {
43 const { owner, repo: repoName } = route.params;
44 const { repo, isLoading: repoLoading, error: repoError, refresh: refreshRepo } = useRepo(owner, repoName);
45 const {
46 entries,
47 isLoading: treeLoading,
48 error: treeError,
49 refresh: refreshTree,
50 } = useTree(owner, repoName, repo?.defaultBranch ?? 'HEAD', '');
51
52 const isLoading = repoLoading || treeLoading;
53 const error = repoError ?? treeError;
54 const refresh = useCallback(() => {
55 refreshRepo();
56 refreshTree();
57 }, [refreshRepo, refreshTree]);
58
59 const handleEntryPress = useCallback(
60 (entry: TreeEntry) => {
61 if (entry.type === 'tree') {
62 navigation.navigate('FileViewer', {
63 owner,
64 repo: repoName,
65 ref: repo?.defaultBranch ?? 'HEAD',
66 path: entry.path,
67 });
68 } else {
69 navigation.navigate('FileViewer', {
70 owner,
71 repo: repoName,
72 ref: repo?.defaultBranch ?? 'HEAD',
73 path: entry.path,
74 });
75 }
76 },
77 [navigation, owner, repoName, repo],
78 );
79
80 const renderEntry = useCallback(
81 ({ item }: { item: TreeEntry }) => (
82 <FileRow entry={item} onPress={handleEntryPress} />
83 ),
84 [handleEntryPress],
85 );
86
87 const keyExtractor = useCallback((item: TreeEntry) => item.path, []);
88
89 if (isLoading) return <LoadingSpinner fullScreen />;
90
91 return (
92 <SafeAreaView style={styles.safe} edges={['bottom']}>
93 <ScrollView showsVerticalScrollIndicator={false}>
94 {error !== null && <ErrorBanner message={error} onRetry={refresh} />}
95
96 {repo !== null && (
97 <>
98 {/* Repo overview card */}
99 <View style={styles.overviewCard}>
100 {repo.description !== null && repo.description.length > 0 && (
101 <Text style={styles.description}>{repo.description}</Text>
102 )}
103 <View style={styles.metaRow}>
104 {repo.language !== null && (
105 <View style={styles.metaItem}>
106 <View style={styles.langDot} />
107 <Text style={styles.metaText}>{repo.language}</Text>
108 </View>
109 )}
110 <View style={styles.metaItem}>
111 <Text style={styles.metaIcon}>★</Text>
112 <Text style={styles.metaText}>{repo.starCount}</Text>
113 </View>
114 <View style={styles.metaItem}>
115 <Text style={styles.metaIcon}>⑂</Text>
116 <Text style={styles.metaText}>{repo.forkCount}</Text>
117 </View>
118 </View>
119 <View style={styles.branchRow}>
120 <Text style={styles.branchIcon}>⑂</Text>
121 <Text style={styles.branchText}>{repo.defaultBranch}</Text>
122 </View>
123 </View>
124
125 {/* Quick nav */}
126 <View style={styles.quickNav}>
127 <TouchableOpacity
128 style={styles.navButton}
129 onPress={() => navigation.navigate('Commits', { owner, repo: repoName })}
130 >
131 <Text style={styles.navIcon}>◎</Text>
132 <Text style={styles.navLabel}>Commits</Text>
133 </TouchableOpacity>
134 <TouchableOpacity
135 style={styles.navButton}
136 onPress={() => navigation.navigate('Issues', { owner, repo: repoName })}
137 >
138 <Text style={styles.navIcon}>○</Text>
139 <Text style={styles.navLabel}>Issues</Text>
140 {repo.openIssueCount > 0 && (
141 <View style={styles.countBadge}>
142 <Text style={styles.countText}>{repo.openIssueCount}</Text>
143 </View>
144 )}
145 </TouchableOpacity>
146 <TouchableOpacity
147 style={styles.navButton}
148 onPress={() => navigation.navigate('Pulls', { owner, repo: repoName })}
149 >
150 <Text style={styles.navIcon}>⑂</Text>
151 <Text style={styles.navLabel}>PRs</Text>
152 </TouchableOpacity>
153 <TouchableOpacity
154 style={styles.navButton}
155 onPress={() => navigation.navigate('GateStatus', { owner, repo: repoName })}
156 >
157 <Text style={styles.navIcon}>⚡</Text>
158 <Text style={styles.navLabel}>Gates</Text>
159 </TouchableOpacity>
160 </View>
161 </>
162 )}
163
164 {/* File tree */}
165 <View style={styles.treeSection}>
166 <View style={styles.treeSectionHeader}>
167 <Text style={styles.treeSectionTitle}>Files</Text>
168 </View>
169 <View style={styles.treeContainer}>
170 <FlatList
171 data={entries}
172 keyExtractor={keyExtractor}
173 renderItem={renderEntry}
174 scrollEnabled={false}
175 ListEmptyComponent={
176 <View style={styles.emptyState}>
177 <Text style={styles.emptyText}>No files found</Text>
178 </View>
179 }
180 />
181 </View>
182 </View>
183 </ScrollView>
184 </SafeAreaView>
185 );
186}
187
188const styles = StyleSheet.create({
189 safe: {
190 flex: 1,
191 backgroundColor: colors.bg,
192 },
193 overviewCard: {
194 backgroundColor: colors.bgSecondary,
195 borderBottomWidth: 1,
196 borderBottomColor: colors.border,
197 padding: 16,
198 gap: 10,
199 },
200 description: {
201 fontSize: 14,
202 color: colors.text,
203 lineHeight: 20,
204 },
205 metaRow: {
206 flexDirection: 'row',
207 flexWrap: 'wrap',
208 gap: 12,
209 },
210 metaItem: {
211 flexDirection: 'row',
212 alignItems: 'center',
213 gap: 4,
214 },
215 langDot: {
216 width: 10,
217 height: 10,
218 borderRadius: 5,
219 backgroundColor: colors.accentPurple,
220 },
221 metaIcon: {
222 fontSize: 12,
223 color: colors.textMuted,
224 },
225 metaText: {
226 fontSize: 13,
227 color: colors.textMuted,
228 },
229 branchRow: {
230 flexDirection: 'row',
231 alignItems: 'center',
232 gap: 6,
233 paddingHorizontal: 10,
234 paddingVertical: 5,
235 backgroundColor: colors.bg,
236 borderRadius: 6,
237 borderWidth: 1,
238 borderColor: colors.border,
239 alignSelf: 'flex-start',
240 },
241 branchIcon: {
242 fontSize: 12,
243 color: colors.textMuted,
244 },
245 branchText: {
246 fontSize: 13,
247 fontFamily: 'monospace',
248 color: colors.text,
249 },
250 quickNav: {
251 flexDirection: 'row',
252 borderBottomWidth: 1,
253 borderBottomColor: colors.border,
254 backgroundColor: colors.bgSecondary,
255 },
256 navButton: {
257 flex: 1,
258 flexDirection: 'row',
259 alignItems: 'center',
260 justifyContent: 'center',
261 paddingVertical: 14,
262 gap: 6,
263 borderRightWidth: 1,
264 borderRightColor: colors.border,
265 },
266 navIcon: {
267 fontSize: 14,
268 color: colors.textMuted,
269 },
270 navLabel: {
271 fontSize: 13,
272 fontWeight: '500',
273 color: colors.text,
274 },
275 countBadge: {
276 paddingHorizontal: 6,
277 paddingVertical: 1,
278 borderRadius: 10,
279 backgroundColor: colors.accentYellow,
280 minWidth: 18,
281 alignItems: 'center',
282 },
283 countText: {
284 fontSize: 10,
285 fontWeight: '700',
286 color: colors.bg,
287 },
288 treeSection: {
289 marginTop: 8,
290 },
291 treeSectionHeader: {
292 paddingHorizontal: 16,
293 paddingVertical: 10,
294 },
295 treeSectionTitle: {
296 fontSize: 12,
297 fontWeight: '600',
298 color: colors.textMuted,
299 textTransform: 'uppercase',
300 letterSpacing: 0.5,
301 },
302 treeContainer: {
303 backgroundColor: colors.bgSecondary,
304 borderTopWidth: 1,
305 borderBottomWidth: 1,
306 borderColor: colors.border,
307 },
308 fileRow: {
309 flexDirection: 'row',
310 alignItems: 'center',
311 paddingVertical: 10,
312 paddingHorizontal: 16,
313 borderBottomWidth: 1,
314 borderBottomColor: colors.border,
315 gap: 10,
316 },
317 fileIcon: {
318 fontSize: 15,
319 },
320 fileName: {
321 flex: 1,
322 fontSize: 14,
323 color: colors.textLink,
324 },
325 chevron: {
326 fontSize: 18,
327 color: colors.textMuted,
328 },
329 emptyState: {
330 padding: 24,
331 alignItems: 'center',
332 },
333 emptyText: {
334 fontSize: 14,
335 color: colors.textMuted,
336 },
337});