Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

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

RepoDetailScreen.tsxBlame323 lines · 1 contributor
c53cf00Claude1import React, { useState } from 'react';
2import {
3 View,
4 Text,
5 ScrollView,
6 StyleSheet,
7 TouchableOpacity,
8 FlatList,
9} from 'react-native';
10import { SafeAreaView } from 'react-native-safe-area-context';
11import { type NativeStackNavigationProp } from '@react-navigation/native-stack';
12import { type RouteProp } from '@react-navigation/native';
13import { colors } from '../theme/colors';
14import { fontSizes, fontWeights, fonts } from '../theme/typography';
15import { useRepo, useFileTree, useCommits } from '../hooks/useRepo';
16import { useIssues } from '../hooks/useIssues';
17import { usePulls } from '../hooks/usePulls';
18import { LoadingSpinner } from '../components/LoadingSpinner';
19import { ErrorState } from '../components/ErrorState';
20import { CommitRow } from '../components/CommitRow';
21import { IssueRow } from '../components/IssueRow';
22import { PullRow } from '../components/PullRow';
9d7a803Claude23import { type MainStackParamList } from '../navigation/types';
c53cf00Claude24
25type Props = {
26 navigation: NativeStackNavigationProp<MainStackParamList, 'RepoDetail'>;
27 route: RouteProp<MainStackParamList, 'RepoDetail'>;
28};
29
30type Tab = 'Code' | 'Issues' | 'PRs' | 'Commits';
31
32const TABS: Tab[] = ['Code', 'Issues', 'PRs', 'Commits'];
33
34export function RepoDetailScreen({ navigation, route }: Props) {
35 const { owner, repo: repoName } = route.params;
36 const [activeTab, setActiveTab] = useState<Tab>('Code');
37
38 const { repo, loading: repoLoading, error: repoError } = useRepo(owner, repoName);
39 const { tree, loading: treeLoading } = useFileTree(owner, repoName, repo?.defaultBranch || 'HEAD');
40 const { commits, loading: commitsLoading } = useCommits(owner, repoName);
41 const { issues, loading: issuesLoading } = useIssues(owner, repoName, 'open');
42 const { pulls, loading: pullsLoading } = usePulls(owner, repoName, 'open');
43
44 if (repoLoading) return <LoadingSpinner fullScreen />;
45 if (repoError || !repo) return <ErrorState message={repoError || 'Repo not found'} />;
46
47 const sorted = [...tree].sort((a, b) => {
48 if (a.type === 'tree' && b.type !== 'tree') return -1;
49 if (a.type !== 'tree' && b.type === 'tree') return 1;
50 return a.name.localeCompare(b.name);
51 });
52
53 return (
54 <SafeAreaView style={styles.safe} edges={['top']}>
55 <ScrollView stickyHeaderIndices={[1]}>
56 {/* Repo header */}
57 <View style={styles.header}>
58 <Text style={styles.repoName}>
59 <Text style={styles.owner}>{owner}/</Text>
60 {repo.name}
61 </Text>
62 {repo.description ? (
63 <Text style={styles.desc}>{repo.description}</Text>
64 ) : null}
65
66 <View style={styles.statsRow}>
67 <View style={styles.stat}>
68 <Text style={styles.statIcon}>★</Text>
69 <Text style={styles.statVal}>{repo.starCount}</Text>
70 </View>
71 <View style={styles.stat}>
72 <Text style={styles.statIcon}>⑂</Text>
73 <Text style={styles.statVal}>{repo.forkCount}</Text>
74 </View>
75 <View style={styles.stat}>
76 <Text style={styles.statIcon}>◉</Text>
77 <Text style={styles.statVal}>{repo.issueCount} issues</Text>
78 </View>
79 <View style={styles.branchBadge}>
80 <Text style={styles.branchText}>{repo.defaultBranch}</Text>
81 </View>
82 </View>
83 </View>
84
85 {/* Tab bar — sticky */}
86 <View style={styles.tabBar}>
87 {TABS.map((tab) => (
88 <TouchableOpacity
89 key={tab}
90 style={[styles.tab, activeTab === tab && styles.tabActive]}
91 onPress={() => setActiveTab(tab)}
92 activeOpacity={0.75}
93 >
94 <Text style={[styles.tabText, activeTab === tab && styles.tabTextActive]}>
95 {tab}
96 </Text>
97 </TouchableOpacity>
98 ))}
99 </View>
100
101 {/* Code tab */}
102 {activeTab === 'Code' && (
103 <View style={styles.fileList}>
104 {treeLoading ? (
105 <LoadingSpinner size="small" />
106 ) : sorted.length === 0 ? (
107 <Text style={styles.emptyText}>Empty repository</Text>
108 ) : (
109 sorted.map((entry) => (
110 <TouchableOpacity
111 key={entry.path}
112 style={styles.fileRow}
113 activeOpacity={0.75}
114 onPress={() => {
115 if (entry.type === 'blob') {
116 navigation.navigate('FileViewer', {
117 owner,
118 repo: repoName,
119 path: entry.path,
120 ref: repo.defaultBranch,
121 });
122 }
123 }}
124 >
125 <Text style={styles.fileIcon}>{entry.type === 'tree' ? '📁' : '📄'}</Text>
126 <Text style={styles.fileName}>{entry.name}</Text>
127 {entry.size !== undefined && entry.type === 'blob' && (
128 <Text style={styles.fileSize}>{formatSize(entry.size)}</Text>
129 )}
130 </TouchableOpacity>
131 ))
132 )}
133 </View>
134 )}
135
136 {/* Issues tab */}
137 {activeTab === 'Issues' && (
138 <View>
139 {issuesLoading ? (
140 <LoadingSpinner size="small" />
141 ) : issues.length === 0 ? (
142 <Text style={styles.emptyText}>No open issues</Text>
143 ) : (
144 issues.map((issue) => (
145 <IssueRow
146 key={issue.id}
147 issue={issue}
148 onPress={() =>
149 navigation.navigate('IssueDetail', {
150 owner,
151 repo: repoName,
152 number: issue.number,
153 })
154 }
155 />
156 ))
157 )}
158 </View>
159 )}
160
161 {/* PRs tab */}
162 {activeTab === 'PRs' && (
163 <View>
164 {pullsLoading ? (
165 <LoadingSpinner size="small" />
166 ) : pulls.length === 0 ? (
167 <Text style={styles.emptyText}>No open pull requests</Text>
168 ) : (
169 pulls.map((pull) => (
170 <PullRow
171 key={pull.id}
172 pull={pull}
173 onPress={() =>
174 navigation.navigate('PullDetail', {
175 owner,
176 repo: repoName,
177 number: pull.number,
178 })
179 }
180 />
181 ))
182 )}
183 </View>
184 )}
185
186 {/* Commits tab */}
187 {activeTab === 'Commits' && (
188 <View>
189 {commitsLoading ? (
190 <LoadingSpinner size="small" />
191 ) : commits.length === 0 ? (
192 <Text style={styles.emptyText}>No commits yet</Text>
193 ) : (
194 commits.map((commit) => <CommitRow key={commit.sha} commit={commit} />)
195 )}
196 </View>
197 )}
198 </ScrollView>
199 </SafeAreaView>
200 );
201}
202
203function formatSize(bytes: number): string {
204 if (bytes < 1024) return `${bytes} B`;
205 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
206 return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
207}
208
209const styles = StyleSheet.create({
210 safe: {
211 flex: 1,
212 backgroundColor: colors.bg,
213 },
214 header: {
215 padding: 16,
216 borderBottomWidth: 1,
217 borderBottomColor: colors.border,
218 },
219 repoName: {
220 fontSize: fontSizes.xl,
221 fontWeight: fontWeights.bold,
222 color: colors.text,
223 marginBottom: 6,
224 },
225 owner: {
226 color: colors.textMuted,
227 fontWeight: fontWeights.regular,
228 },
229 desc: {
230 color: colors.textMuted,
231 fontSize: fontSizes.sm,
232 lineHeight: 18,
233 marginBottom: 12,
234 },
235 statsRow: {
236 flexDirection: 'row',
237 alignItems: 'center',
238 gap: 12,
239 flexWrap: 'wrap',
240 },
241 stat: {
242 flexDirection: 'row',
243 alignItems: 'center',
244 gap: 4,
245 },
246 statIcon: {
247 color: colors.textMuted,
248 fontSize: fontSizes.sm,
249 },
250 statVal: {
251 color: colors.textMuted,
252 fontSize: fontSizes.sm,
253 },
254 branchBadge: {
255 backgroundColor: colors.bgSecondary,
256 borderRadius: 12,
257 paddingHorizontal: 8,
258 paddingVertical: 3,
259 borderWidth: 1,
260 borderColor: colors.border,
261 },
262 branchText: {
263 color: colors.textMuted,
264 fontSize: fontSizes.xs,
265 fontFamily: fonts.mono,
266 },
267 tabBar: {
268 flexDirection: 'row',
269 backgroundColor: colors.bg,
270 borderBottomWidth: 1,
271 borderBottomColor: colors.border,
272 },
273 tab: {
274 flex: 1,
275 paddingVertical: 12,
276 alignItems: 'center',
277 },
278 tabActive: {
279 borderBottomWidth: 2,
280 borderBottomColor: colors.accent,
281 },
282 tabText: {
283 color: colors.textMuted,
284 fontSize: fontSizes.sm,
285 fontWeight: fontWeights.medium,
286 },
287 tabTextActive: {
288 color: colors.accent,
289 },
290 fileList: {
291 padding: 4,
292 },
293 fileRow: {
294 flexDirection: 'row',
295 alignItems: 'center',
296 paddingHorizontal: 16,
297 paddingVertical: 10,
298 borderBottomWidth: 1,
299 borderBottomColor: colors.border,
300 gap: 10,
301 },
302 fileIcon: {
303 fontSize: 16,
304 width: 20,
305 textAlign: 'center',
306 },
307 fileName: {
308 flex: 1,
309 color: colors.text,
310 fontSize: fontSizes.sm,
311 fontFamily: fonts.mono,
312 },
313 fileSize: {
314 color: colors.textMuted,
315 fontSize: fontSizes.xs,
316 },
317 emptyText: {
318 color: colors.textMuted,
319 fontSize: fontSizes.sm,
320 textAlign: 'center',
321 padding: 32,
322 },
323});