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

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

LoginScreen.tsxBlame249 lines · 1 contributor
9a8fbedClaude1import React, { useState, useCallback } from 'react';
2import {
3 KeyboardAvoidingView,
4 Platform,
5 ScrollView,
6 StyleSheet,
7 Text,
8 TextInput,
9 TouchableOpacity,
10 View,
11 ActivityIndicator,
12} from 'react-native';
13import { StatusBar } from 'expo-status-bar';
14import { useAuth } from '../hooks/useAuth';
15import { colors } from '../theme/colors';
16
17export function LoginScreen(): React.ReactElement {
18 const { login, isLoading, error } = useAuth();
19
20 const [host, setHost] = useState('https://gluecron.com');
21 const [token, setToken] = useState('');
22 const [localError, setLocalError] = useState<string | null>(null);
23
24 const handleConnect = useCallback(async () => {
25 setLocalError(null);
26 const trimmedHost = host.trim();
27 const trimmedToken = token.trim();
28
29 if (!trimmedHost) {
30 setLocalError('Host URL is required');
31 return;
32 }
33 if (!trimmedToken) {
34 setLocalError('Personal Access Token is required');
35 return;
36 }
37 if (!trimmedToken.startsWith('glc_')) {
38 setLocalError('Token must start with glc_');
39 return;
40 }
41
42 try {
43 await login(trimmedHost, trimmedToken);
44 } catch (err) {
45 setLocalError(err instanceof Error ? err.message : 'Login failed');
46 }
47 }, [host, token, login]);
48
49 const displayError = localError ?? error;
50
51 return (
52 <KeyboardAvoidingView
53 style={styles.flex}
54 behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
55 >
56 <StatusBar style="light" />
57 <ScrollView
58 contentContainerStyle={styles.container}
59 keyboardShouldPersistTaps="handled"
60 >
61 {/* Branding */}
62 <View style={styles.brandSection}>
63 <View style={styles.logoContainer}>
64 <Text style={styles.logoText}>⑂</Text>
65 </View>
66 <Text style={styles.appName}>Gluecron</Text>
67 <Text style={styles.tagline}>AI-native code intelligence platform</Text>
68 </View>
69
70 {/* Form */}
71 <View style={styles.card}>
72 <Text style={styles.cardTitle}>Connect to your instance</Text>
73
74 <View style={styles.formGroup}>
75 <Text style={styles.label}>Host URL</Text>
76 <TextInput
77 style={styles.input}
78 value={host}
79 onChangeText={setHost}
80 placeholder="https://gluecron.com"
81 placeholderTextColor={colors.textMuted}
82 autoCapitalize="none"
83 autoCorrect={false}
84 keyboardType="url"
85 textContentType="URL"
86 />
87 </View>
88
89 <View style={styles.formGroup}>
90 <Text style={styles.label}>Personal Access Token</Text>
91 <TextInput
92 style={styles.input}
93 value={token}
94 onChangeText={setToken}
95 placeholder="glc_..."
96 placeholderTextColor={colors.textMuted}
97 autoCapitalize="none"
98 autoCorrect={false}
99 secureTextEntry
100 textContentType="password"
101 />
102 <Text style={styles.hint}>
103 Generate a token in Settings → Personal Access Tokens on your Gluecron instance.
104 </Text>
105 </View>
106
107 {displayError !== null && (
108 <View style={styles.errorBox}>
109 <Text style={styles.errorText}>{displayError}</Text>
110 </View>
111 )}
112
113 <TouchableOpacity
114 style={[styles.button, isLoading && styles.buttonDisabled]}
115 onPress={handleConnect}
116 disabled={isLoading}
117 activeOpacity={0.8}
118 >
119 {isLoading ? (
120 <ActivityIndicator size="small" color={colors.text} />
121 ) : (
122 <Text style={styles.buttonText}>Connect</Text>
123 )}
124 </TouchableOpacity>
125 </View>
126
127 {/* Footer */}
128 <Text style={styles.footer}>
129 gluecron.com — git hosting, AI code review, gate enforcement
130 </Text>
131 </ScrollView>
132 </KeyboardAvoidingView>
133 );
134}
135
136const styles = StyleSheet.create({
137 flex: {
138 flex: 1,
139 backgroundColor: colors.bg,
140 },
141 container: {
142 flexGrow: 1,
143 justifyContent: 'center',
144 paddingHorizontal: 24,
145 paddingVertical: 48,
146 gap: 32,
147 },
148 brandSection: {
149 alignItems: 'center',
150 gap: 10,
151 },
152 logoContainer: {
153 width: 72,
154 height: 72,
155 borderRadius: 18,
156 backgroundColor: colors.bgSecondary,
157 borderWidth: 1,
158 borderColor: colors.border,
159 alignItems: 'center',
160 justifyContent: 'center',
161 },
162 logoText: {
163 fontSize: 36,
164 color: colors.accentBlue,
165 },
166 appName: {
167 fontSize: 28,
168 fontWeight: '700',
169 color: colors.text,
170 letterSpacing: -0.5,
171 },
172 tagline: {
173 fontSize: 14,
174 color: colors.textMuted,
175 textAlign: 'center',
176 },
177 card: {
178 backgroundColor: colors.bgSecondary,
179 borderRadius: 12,
180 borderWidth: 1,
181 borderColor: colors.border,
182 padding: 20,
183 gap: 16,
184 },
185 cardTitle: {
186 fontSize: 16,
187 fontWeight: '600',
188 color: colors.text,
189 marginBottom: 4,
190 },
191 formGroup: {
192 gap: 6,
193 },
194 label: {
195 fontSize: 13,
196 fontWeight: '500',
197 color: colors.textMuted,
198 textTransform: 'uppercase',
199 letterSpacing: 0.5,
200 },
201 input: {
202 backgroundColor: colors.bg,
203 borderWidth: 1,
204 borderColor: colors.border,
205 borderRadius: 8,
206 paddingHorizontal: 14,
207 paddingVertical: 12,
208 fontSize: 15,
209 color: colors.text,
210 },
211 hint: {
212 fontSize: 12,
213 color: colors.textMuted,
214 lineHeight: 16,
215 },
216 errorBox: {
217 backgroundColor: colors.bg,
218 borderWidth: 1,
219 borderColor: colors.accentRed,
220 borderRadius: 8,
221 padding: 12,
222 },
223 errorText: {
224 fontSize: 13,
225 color: colors.accentRed,
226 lineHeight: 18,
227 },
228 button: {
229 backgroundColor: colors.accent,
230 borderRadius: 8,
231 paddingVertical: 14,
232 alignItems: 'center',
233 justifyContent: 'center',
234 minHeight: 48,
235 },
236 buttonDisabled: {
237 opacity: 0.6,
238 },
239 buttonText: {
240 fontSize: 15,
241 fontWeight: '600',
242 color: colors.text,
243 },
244 footer: {
245 fontSize: 12,
246 color: colors.textMuted,
247 textAlign: 'center',
248 },
249});