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

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

IssuesScreen.tsxBlame176 lines · 1 contributor
9a8fbedClaude1import React, { useCallback, useEffect, useState } from 'react';
2import {
3 FlatList,
4 StyleSheet,
5 Text,
6 TouchableOpacity,
7 View,
8} from 'react-native';
9import { SafeAreaView } from 'react-native-safe-area-context';
10import type { NativeStackScreenProps } from '@react-navigation/native-stack';
11import { listIssues } from '../api/issues';
12import type { Issue } from '../api/issues';
13import { IssueRow } from '../components/IssueRow';
14import { LoadingSpinner } from '../components/LoadingSpinner';
15import { ErrorBanner } from '../components/ErrorBanner';
16import { colors } from '../theme/colors';
17import type { RepoStackParamList } from '../navigation/AppNavigator';
18
19type Props = NativeStackScreenProps<RepoStackParamList, 'Issues'>;
20type FilterState = 'open' | 'closed';
21
22export function IssuesScreen({ route, navigation }: Props): React.ReactElement {
23 const { owner, repo } = route.params;
24 const [filter, setFilter] = useState<FilterState>('open');
25 const [issues, setIssues] = useState<Issue[]>([]);
26 const [isLoading, setIsLoading] = useState(true);
27 const [error, setError] = useState<string | null>(null);
28 const [page, setPage] = useState(1);
29 const [hasMore, setHasMore] = useState(true);
30
31 useEffect(() => {
32 let cancelled = false;
33 setIsLoading(true);
34 setError(null);
35
36 listIssues(owner, repo, filter, 1)
37 .then((data) => {
38 if (!cancelled) {
39 setIssues(data);
40 setPage(1);
41 setHasMore(data.length === 30);
42 }
43 })
44 .catch((err) => {
45 if (!cancelled) {
46 setError(err instanceof Error ? err.message : 'Failed to load issues');
47 }
48 })
49 .finally(() => {
50 if (!cancelled) setIsLoading(false);
51 });
52
53 return () => {
54 cancelled = true;
55 };
56 }, [owner, repo, filter]);
57
58 const loadMore = useCallback(() => {
59 if (!hasMore || isLoading) return;
60 const nextPage = page + 1;
61
62 listIssues(owner, repo, filter, nextPage)
63 .then((data) => {
64 setIssues((prev) => [...prev, ...data]);
65 setPage(nextPage);
66 setHasMore(data.length === 30);
67 })
68 .catch(() => {/* ignore pagination errors */});
69 }, [owner, repo, filter, page, hasMore, isLoading]);
70
71 const handleIssuePress = useCallback(
72 (issue: Issue) => {
73 navigation.navigate('IssueDetail', { owner, repo, number: issue.number });
74 },
75 [navigation, owner, repo],
76 );
77
78 const renderIssue = useCallback(
79 ({ item }: { item: Issue }) => <IssueRow issue={item} onPress={handleIssuePress} />,
80 [handleIssuePress],
81 );
82
83 const keyExtractor = useCallback((item: Issue) => String(item.id), []);
84
85 const renderFooter = useCallback(
86 () => (hasMore ? <LoadingSpinner size="small" /> : null),
87 [hasMore],
88 );
89
90 if (isLoading && issues.length === 0) return <LoadingSpinner fullScreen />;
91
92 return (
93 <SafeAreaView style={styles.safe} edges={['bottom']}>
94 {/* Filter tabs */}
95 <View style={styles.filterTabs}>
96 <TouchableOpacity
97 style={[styles.filterTab, filter === 'open' && styles.filterTabActive]}
98 onPress={() => setFilter('open')}
99 >
100 <Text style={[styles.filterTabText, filter === 'open' && styles.filterTabTextActive]}>
101 Open
102 </Text>
103 </TouchableOpacity>
104 <TouchableOpacity
105 style={[styles.filterTab, filter === 'closed' && styles.filterTabActive]}
106 onPress={() => setFilter('closed')}
107 >
108 <Text style={[styles.filterTabText, filter === 'closed' && styles.filterTabTextActive]}>
109 Closed
110 </Text>
111 </TouchableOpacity>
112 </View>
113
114 {error !== null && <ErrorBanner message={error} />}
115
116 <FlatList
117 data={issues}
118 keyExtractor={keyExtractor}
119 renderItem={renderIssue}
120 onEndReached={loadMore}
121 onEndReachedThreshold={0.3}
122 ListFooterComponent={renderFooter}
123 ListEmptyComponent={
124 <View style={styles.emptyState}>
125 <Text style={styles.emptyText}>
126 No {filter} issues
127 </Text>
128 </View>
129 }
130 style={styles.list}
131 />
132 </SafeAreaView>
133 );
134}
135
136const styles = StyleSheet.create({
137 safe: {
138 flex: 1,
139 backgroundColor: colors.bg,
140 },
141 filterTabs: {
142 flexDirection: 'row',
143 backgroundColor: colors.bgSecondary,
144 borderBottomWidth: 1,
145 borderBottomColor: colors.border,
146 },
147 filterTab: {
148 flex: 1,
149 paddingVertical: 12,
150 alignItems: 'center',
151 borderBottomWidth: 2,
152 borderBottomColor: 'transparent',
153 },
154 filterTabActive: {
155 borderBottomColor: colors.accentBlue,
156 },
157 filterTabText: {
158 fontSize: 14,
159 fontWeight: '500',
160 color: colors.textMuted,
161 },
162 filterTabTextActive: {
163 color: colors.text,
164 },
165 list: {
166 flex: 1,
167 },
168 emptyState: {
169 padding: 40,
170 alignItems: 'center',
171 },
172 emptyText: {
173 fontSize: 14,
174 color: colors.textMuted,
175 },
176});