Greenlens/app/auth/login.tsx

289 lines
8.4 KiB
TypeScript

import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
ScrollView,
Image,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { AuthService } from '../../services/authService';
export default function LoginScreen() {
const { isDarkMode, colorPalette, hydrateSession } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLogin = async () => {
if (!email.trim() || !password) {
setError('Bitte alle Felder ausfüllen.');
return;
}
setLoading(true);
setError(null);
try {
const session = await AuthService.login(email, password);
await hydrateSession(session);
router.replace('/(tabs)');
} catch (e: any) {
if (e.message === 'USER_NOT_FOUND') {
setError('Kein Konto mit dieser E-Mail gefunden.');
} else if (e.message === 'WRONG_PASSWORD') {
setError('Falsches Passwort.');
} else if (e.message === 'BACKEND_URL_MISSING') {
setError('Backend-URL fehlt. Bitte EXPO_PUBLIC_BACKEND_URL konfigurieren.');
} else if (e.message === 'NETWORK_ERROR') {
setError('Server nicht erreichbar. Bitte versuche es erneut.');
} else {
setError('Anmeldung fehlgeschlagen. Bitte versuche es erneut.');
}
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={[styles.flex, { backgroundColor: colors.background }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ThemeBackdrop colors={colors} />
<ScrollView
contentContainerStyle={styles.scroll}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Logo / Header */}
<View style={styles.header}>
<Image
source={require('../../assets/icon.png')}
style={styles.logoIcon}
resizeMode="contain"
/>
<Text style={[styles.appName, { color: colors.text }]}>GreenLens</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
Willkommen zurück
</Text>
</View>
{/* Card */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.cardBorder, shadowColor: colors.cardShadow }]}>
{/* Email */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>E-Mail</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder="deine@email.de"
placeholderTextColor={colors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
returnKeyType="next"
/>
</View>
</View>
{/* Password */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>Passwort</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder="••••••••"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoComplete="password"
returnKeyType="done"
onSubmitEditing={handleLogin}
/>
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
<Ionicons
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
size={18}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
</View>
{/* Error */}
{error && (
<View style={[styles.errorBox, { backgroundColor: colors.dangerSoft }]}>
<Ionicons name="alert-circle-outline" size={15} color={colors.danger} />
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
</View>
)}
{/* Login Button */}
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.7 : 1 }]}
onPress={handleLogin}
activeOpacity={0.82}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>Anmelden</Text>
)}
</TouchableOpacity>
</View>
{/* Divider */}
<View style={styles.dividerRow}>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
<Text style={[styles.dividerText, { color: colors.textMuted }]}>oder</Text>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
</View>
{/* Sign Up Link */}
<TouchableOpacity
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
onPress={() => router.replace('/auth/signup')}
activeOpacity={0.82}
>
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>
Noch kein Konto?{' '}
<Text style={{ color: colors.primary, fontWeight: '600' }}>Registrieren</Text>
</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
flex: { flex: 1 },
scroll: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: 24,
paddingVertical: 48,
},
header: {
alignItems: 'center',
marginBottom: 32,
},
logoIcon: {
width: 56,
height: 56,
borderRadius: 14,
marginBottom: 16,
},
appName: {
fontSize: 30,
fontWeight: '700',
letterSpacing: -0.5,
marginBottom: 6,
},
subtitle: {
fontSize: 15,
fontWeight: '400',
},
card: {
borderRadius: 20,
borderWidth: 1,
padding: 24,
gap: 16,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 1,
shadowRadius: 12,
elevation: 4,
},
fieldGroup: {
gap: 6,
},
label: {
fontSize: 13,
fontWeight: '500',
marginLeft: 2,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
height: 50,
},
eyeBtn: {
padding: 4,
marginLeft: 6,
},
errorBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
},
errorText: {
fontSize: 13,
flex: 1,
},
primaryBtn: {
height: 52,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginTop: 4,
},
primaryBtnText: {
fontSize: 16,
fontWeight: '600',
},
dividerRow: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
gap: 12,
},
dividerLine: {
flex: 1,
height: 1,
},
dividerText: {
fontSize: 13,
},
secondaryBtn: {
height: 52,
borderRadius: 14,
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
},
secondaryBtnText: {
fontSize: 15,
},
});