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

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

DashboardScreen.tsxBlame313 lines · 1 contributor
9a8fbedClaude1import React, { useCallback, useEffect, useState } from 'react';
2import {
3 FlatList,
4 ScrollView,
5 StyleSheet,
6 Text,
7 TouchableOpacity,
8 View,
9} from 'react-native';
10import { StatusBar } from 'expo-status-bar';
11import { SafeAreaView } from 'react-native-safe-area-context';
12import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
13import { useAuthStore } from '../store/authStore';
14import { listUserRepos } from '../api/repos';
15import { getNotificationCounts } from '../api/notifications';
16import type { Repository } from '../api/repos';
17import { RepoCard } from '../components/RepoCard';
18import { LoadingSpinner } from '../components/LoadingSpinner';
19import { ErrorBanner } from '../components/ErrorBanner';
20import { colors } from '../theme/colors';
21import type { MainTabParamList } from '../navigation/AppNavigator';
22
23type Props = BottomTabScreenProps<MainTabParamList, 'Dashboard'>;
24
25export function DashboardScreen({ navigation }: Props): React.ReactElement {
26 const user = useAuthStore((s) => s.user);
27 const [repos, setRepos] = useState<Repository[]>([]);
28 const [notifCount, setNotifCount] = useState(0);
29 const [isLoading, setIsLoading] = useState(true);
30 const [error, setError] = useState<string | null>(null);
31 const [tick, setTick] = useState(0);
32
33 const greenCount = repos.filter((r) => r.openIssueCount === 0).length;
34
35 useEffect(() => {
36 if (!user) return;
37 let cancelled = false;
38
39 setIsLoading(true);
40 setError(null);
41
42 Promise.all([
43 listUserRepos(user.username),
44 getNotificationCounts().catch(() => ({ total: 0, unread: 0 })),
45 ])
46 .then(([repoList, counts]) => {
47 if (!cancelled) {
48 setRepos(repoList.slice(0, 10));
49 setNotifCount(counts.unread);
50 }
51 })
52 .catch((err) => {
53 if (!cancelled) {
54 setError(err instanceof Error ? err.message : 'Failed to load dashboard');
55 }
56 })
57 .finally(() => {
58 if (!cancelled) setIsLoading(false);
59 });
60
61 return () => {
62 cancelled = true;
63 };
64 }, [user, tick]);
65
66 const retry = useCallback(() => setTick((t) => t + 1), []);
67
68 const handleRepoPress = useCallback(
69 (repo: Repository) => {
70 (navigation as any).navigate('Repos', {
71 screen: 'Repo',
72 params: { owner: repo.ownerUsername, repo: repo.name },
73 });
74 },
75 [navigation],
76 );
77
78 const navigateNotifications = useCallback(() => {
79 (navigation as any).navigate('Notifications');
80 }, [navigation]);
81
82 const navigateAskAI = useCallback(() => {
83 (navigation as any).navigate('AskAI');
84 }, [navigation]);
85
86 const renderRepo = useCallback(
87 ({ item }: { item: Repository }) => (
88 <RepoCard repo={item} onPress={handleRepoPress} />
89 ),
90 [handleRepoPress],
91 );
92
93 const keyExtractor = useCallback((item: Repository) => String(item.id), []);
94
95 if (isLoading) return <LoadingSpinner fullScreen />;
96
97 return (
98 <SafeAreaView style={styles.safe} edges={['top']}>
99 <StatusBar style="light" />
100
101 {/* Header */}
102 <View style={styles.header}>
103 <View>
104 <Text style={styles.greeting}>
105 Hello, {user?.username ?? 'there'} 👋
106 </Text>
107 <Text style={styles.subtitle}>Your Gluecron dashboard</Text>
108 </View>
109 <TouchableOpacity style={styles.notifButton} onPress={navigateNotifications}>
110 <Text style={styles.notifIcon}>🔔</Text>
111 {notifCount > 0 && (
112 <View style={styles.badge}>
113 <Text style={styles.badgeText}>
114 {notifCount > 99 ? '99+' : String(notifCount)}
115 </Text>
116 </View>
117 )}
118 </TouchableOpacity>
119 </View>
120
121 {error !== null && <ErrorBanner message={error} onRetry={retry} />}
122
123 <ScrollView style={styles.flex} showsVerticalScrollIndicator={false}>
124 {/* Stats row */}
125 <View style={styles.statsRow}>
126 <View style={styles.statCard}>
127 <Text style={styles.statValue}>{repos.length}</Text>
128 <Text style={styles.statLabel}>Repositories</Text>
129 </View>
130 <View style={styles.statCard}>
131 <Text style={[styles.statValue, { color: colors.accent }]}>{greenCount}</Text>
132 <Text style={styles.statLabel}>Gates Green</Text>
133 </View>
134 <View style={styles.statCard}>
135 <Text style={[styles.statValue, { color: colors.accentRed }]}>
136 {repos.length - greenCount}
137 </Text>
138 <Text style={styles.statLabel}>Need Attention</Text>
139 </View>
140 </View>
141
142 {/* Recent repos */}
143 <View style={styles.sectionHeader}>
144 <Text style={styles.sectionTitle}>Recent Repositories</Text>
145 <TouchableOpacity
146 onPress={() => (navigation as any).navigate('Repos')}
147 >
148 <Text style={styles.seeAll}>See all</Text>
149 </TouchableOpacity>
150 </View>
151
152 <FlatList
153 data={repos}
154 keyExtractor={keyExtractor}
155 renderItem={renderRepo}
156 scrollEnabled={false}
157 ListEmptyComponent={
158 <View style={styles.emptyState}>
159 <Text style={styles.emptyText}>No repositories yet</Text>
160 </View>
161 }
162 />
163
164 {/* Spacer for FAB */}
165 <View style={styles.fabSpacer} />
166 </ScrollView>
167
168 {/* Ask AI FAB */}
169 <TouchableOpacity style={styles.fab} onPress={navigateAskAI} activeOpacity={0.85}>
170 <Text style={styles.fabIcon}>✦</Text>
171 <Text style={styles.fabText}>Ask AI</Text>
172 </TouchableOpacity>
173 </SafeAreaView>
174 );
175}
176
177const styles = StyleSheet.create({
178 safe: {
179 flex: 1,
180 backgroundColor: colors.bg,
181 },
182 flex: {
183 flex: 1,
184 },
185 header: {
186 flexDirection: 'row',
187 alignItems: 'center',
188 justifyContent: 'space-between',
189 paddingHorizontal: 16,
190 paddingVertical: 16,
191 borderBottomWidth: 1,
192 borderBottomColor: colors.border,
193 backgroundColor: colors.bgSecondary,
194 },
195 greeting: {
196 fontSize: 18,
197 fontWeight: '700',
198 color: colors.text,
199 },
200 subtitle: {
201 fontSize: 13,
202 color: colors.textMuted,
203 marginTop: 2,
204 },
205 notifButton: {
206 width: 44,
207 height: 44,
208 alignItems: 'center',
209 justifyContent: 'center',
210 },
211 notifIcon: {
212 fontSize: 22,
213 },
214 badge: {
215 position: 'absolute',
216 top: 4,
217 right: 4,
218 minWidth: 18,
219 height: 18,
220 borderRadius: 9,
221 backgroundColor: colors.accentRed,
222 alignItems: 'center',
223 justifyContent: 'center',
224 paddingHorizontal: 4,
225 },
226 badgeText: {
227 fontSize: 10,
228 fontWeight: '700',
229 color: colors.text,
230 },
231 statsRow: {
232 flexDirection: 'row',
233 padding: 16,
234 gap: 12,
235 },
236 statCard: {
237 flex: 1,
238 backgroundColor: colors.bgSecondary,
239 borderRadius: 10,
240 borderWidth: 1,
241 borderColor: colors.border,
242 padding: 14,
243 alignItems: 'center',
244 gap: 4,
245 },
246 statValue: {
247 fontSize: 24,
248 fontWeight: '700',
249 color: colors.text,
250 },
251 statLabel: {
252 fontSize: 11,
253 color: colors.textMuted,
254 textAlign: 'center',
255 },
256 sectionHeader: {
257 flexDirection: 'row',
258 alignItems: 'center',
259 justifyContent: 'space-between',
260 paddingHorizontal: 16,
261 paddingBottom: 8,
262 paddingTop: 4,
263 },
264 sectionTitle: {
265 fontSize: 14,
266 fontWeight: '600',
267 color: colors.textMuted,
268 textTransform: 'uppercase',
269 letterSpacing: 0.5,
270 },
271 seeAll: {
272 fontSize: 13,
273 color: colors.textLink,
274 },
275 emptyState: {
276 padding: 32,
277 alignItems: 'center',
278 },
279 emptyText: {
280 fontSize: 14,
281 color: colors.textMuted,
282 },
283 fabSpacer: {
284 height: 80,
285 },
286 fab: {
287 position: 'absolute',
288 bottom: 24,
289 right: 20,
290 flexDirection: 'row',
291 alignItems: 'center',
292 backgroundColor: colors.accentPurple,
293 borderRadius: 28,
294 paddingHorizontal: 20,
295 paddingVertical: 14,
296 gap: 8,
297 shadowColor: colors.accentPurple,
298 shadowOpacity: 0.4,
299 shadowRadius: 12,
300 shadowOffset: { width: 0, height: 4 },
301 elevation: 8,
302 },
303 fabIcon: {
304 fontSize: 16,
305 color: colors.bg,
306 fontWeight: '700',
307 },
308 fabText: {
309 fontSize: 15,
310 fontWeight: '700',
311 color: colors.bg,
312 },
313});