| import React, { useState } from 'react'; |
| import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, KeyboardAvoidingView, Platform, Alert } from 'react-native'; |
| import { MaterialIcons } from '@expo/vector-icons'; |
| import AsyncStorage from '@react-native-async-storage/async-storage'; |
| import { BACKEND_URL } from '../app/utils/syncManager'; |
|
|
| interface Props { |
| onLoginSuccess: (token: string) => void; |
| } |
|
|
| export default function LoginScreen({ onLoginSuccess }: Props) { |
| const [username, setUsername] = useState(''); |
| const [password, setPassword] = useState(''); |
| const [isLoading, setIsLoading] = useState(false); |
|
|
| const handleLogin = async () => { |
| if (!username || !password) { |
| Alert.alert('Error', 'Please enter both username and password.'); |
| return; |
| } |
|
|
| setIsLoading(true); |
| try { |
| const res = await fetch(`${BACKEND_URL}/auth/login`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ username, password }), |
| }); |
| |
| const data = await res.json(); |
| |
| if (res.ok && data.token) { |
| await AsyncStorage.setItem('jwt_token', data.token); |
| onLoginSuccess(data.token); |
| } else { |
| Alert.alert('Login Failed', data.detail || 'Invalid credentials'); |
| } |
| } catch (e) { |
| Alert.alert('Network Error', 'Could not connect to server.'); |
| } finally { |
| setIsLoading(false); |
| } |
| }; |
|
|
| return ( |
| <KeyboardAvoidingView |
| behavior={Platform.OS === 'ios' ? 'padding' : 'height'} |
| style={styles.container} |
| > |
| <View style={styles.card}> |
| <View style={styles.header}> |
| <MaterialIcons name="cloud-sync" size={48} color="#1a73e8" /> |
| <Text style={styles.title}>KeyStone</Text> |
| <Text style={styles.subtitle}>Sign in to your private cloud</Text> |
| </View> |
| |
| <View style={styles.form}> |
| <View style={styles.inputContainer}> |
| <MaterialIcons name="person" size={20} color="#5f6368" style={styles.inputIcon} /> |
| <TextInput |
| style={styles.input} |
| placeholder="Username" |
| placeholderTextColor="#9aa0a6" |
| value={username} |
| onChangeText={setUsername} |
| autoCapitalize="none" |
| autoCorrect={false} |
| /> |
| </View> |
| |
| <View style={styles.inputContainer}> |
| <MaterialIcons name="lock" size={20} color="#5f6368" style={styles.inputIcon} /> |
| <TextInput |
| style={styles.input} |
| placeholder="Password" |
| placeholderTextColor="#9aa0a6" |
| secureTextEntry |
| value={password} |
| onChangeText={setPassword} |
| /> |
| </View> |
| |
| <TouchableOpacity |
| style={[styles.button, isLoading && styles.buttonDisabled]} |
| onPress={handleLogin} |
| disabled={isLoading} |
| > |
| {isLoading ? ( |
| <ActivityIndicator color="#fff" /> |
| ) : ( |
| <Text style={styles.buttonText}>Sign In</Text> |
| )} |
| </TouchableOpacity> |
| </View> |
| </View> |
| </KeyboardAvoidingView> |
| ); |
| } |
|
|
| const styles = StyleSheet.create({ |
| container: { |
| flex: 1, |
| backgroundColor: '#F6EAE1', |
| justifyContent: 'center', |
| padding: 24, |
| }, |
| card: { |
| backgroundColor: '#F6EAE1', |
| borderRadius: 24, |
| padding: 32, |
| shadowColor: '#000', |
| shadowOffset: { width: 0, height: 8 }, |
| shadowOpacity: 0.05, |
| shadowRadius: 24, |
| elevation: 4, |
| }, |
| header: { |
| alignItems: 'center', |
| marginBottom: 32, |
| }, |
| title: { |
| fontSize: 28, |
| fontWeight: '800', |
| color: '#202124', |
| marginTop: 16, |
| letterSpacing: -0.5, |
| }, |
| subtitle: { |
| fontSize: 15, |
| color: '#5f6368', |
| marginTop: 8, |
| }, |
| form: { |
| gap: 16, |
| }, |
| inputContainer: { |
| flexDirection: 'row', |
| alignItems: 'center', |
| backgroundColor: '#f1f3f4', |
| borderRadius: 12, |
| paddingHorizontal: 16, |
| height: 56, |
| }, |
| inputIcon: { |
| marginRight: 12, |
| }, |
| input: { |
| flex: 1, |
| fontSize: 16, |
| color: '#202124', |
| height: '100%', |
| }, |
| button: { |
| backgroundColor: '#1a73e8', |
| height: 56, |
| borderRadius: 12, |
| justifyContent: 'center', |
| alignItems: 'center', |
| marginTop: 8, |
| }, |
| buttonDisabled: { |
| opacity: 0.7, |
| }, |
| buttonText: { |
| color: '#fff', |
| fontSize: 16, |
| fontWeight: '700', |
| }, |
| }); |
|
|