Centralized_Rreporting_System/src/modules/auth/screens/LoginScreen.tsx
2025-09-15 19:59:08 +05:30

460 lines
14 KiB
TypeScript

import React from 'react';
import {
View,
Text,
StyleSheet,
TextInput,
TouchableOpacity,
Pressable,
Alert,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import GradientBackground from '@/shared/components/layout/GradientBackground';
import LinearGradient from 'react-native-linear-gradient';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useDispatch, useSelector } from 'react-redux';
import { login, clearError } from '@/modules/auth/store/authSlice';
import type { RootState, AppDispatch } from '@/store/store';
import { useTheme } from '@/shared/styles/useTheme';
import { validateLoginForm } from '@/shared/utils/validation';
import { showError, showSuccess, showWarning, showInfo } from '@/shared/utils/Toast';
const LoginScreen: React.FC = () => {
const navigation = useNavigation();
const dispatch = useDispatch<AppDispatch>();
const { colors, fonts } = useTheme();
const { loading, error, isAuthenticated } = useSelector((s: RootState) => s.auth);
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const [showPassword, setShowPassword] = React.useState(false);
const [rememberMe, setRememberMe] = React.useState(false);
const [focused, setFocused] = React.useState<null | 'email' | 'password'>(null);
const [validationErrors, setValidationErrors] = React.useState<{ email?: string; password?: string }>({});
const emailRef = React.useRef<TextInput>(null);
const passwordRef = React.useRef<TextInput>(null);
// Clear validation errors when user starts typing
const handleEmailChange = (text: string) => {
setEmail(text);
if (validationErrors.email) {
setValidationErrors(prev => ({ ...prev, email: undefined }));
}
// Clear API error when user starts typing
if (error) {
dispatch(clearError());
}
};
const handlePasswordChange = (text: string) => {
setPassword(text);
if (validationErrors.password) {
setValidationErrors(prev => ({ ...prev, password: undefined }));
}
// Clear API error when user starts typing
if (error) {
dispatch(clearError());
}
};
const handleLogin = async () => {
// Clear previous validation errors
setValidationErrors({});
// Validate form inputs
const validation = validateLoginForm(email, password);
if (!validation.isValid) {
setValidationErrors(validation.errors);
// Show toast messages for validation errors
if (validation.errors.email) {
showError(validation.errors.email);
}
if (validation.errors.password) {
showError(validation.errors.password);
}
return;
}
try {
// Dispatch login action
const result = await dispatch(login({ email, password }));
// Check if login was successful
if (login.fulfilled.match(result)) {
// Login successful - show success toast
showSuccess('Login successful! Welcome back!');
// Navigation will be handled by the app navigator based on isAuthenticated state
} else if (login.rejected.match(result)) {
// Login failed - show error toast
showError(result.payload as string || 'Login failed. Please try again.');
}
} catch (err) {
// Error handling is done in the slice
console.error('Login error:', err);
showError('An unexpected error occurred. Please try again.');
}
};
// Clear error when component mounts or when user starts typing
React.useEffect(() => {
if (error) {
dispatch(clearError());
}
}, [dispatch]);
// Auto-focus email input when component mounts
React.useEffect(() => {
const timer = setTimeout(() => {
emailRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<GradientBackground preset="warm" style={styles.gradient}>
<View style={styles.container}>
{/* Card */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
{/* Logo placeholder */}
<View style={[styles.logoCircle, { backgroundColor: '#F1F5F9' }]}>
<Icon name="shield" size={28} color={colors.primary} />
</View>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Login</Text>
<Text style={[styles.subtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>Enter your email and password to log in</Text>
{/* Email input */}
<View>
<Pressable
style={[
styles.inputWrapper,
{
borderColor: validationErrors.email
? colors.error
: focused === 'email'
? colors.primary
: colors.border,
backgroundColor: colors.surface,
},
]}
onPress={() => emailRef.current?.focus()}
>
<Icon
name="email-outline"
size={20}
color={validationErrors.email
? colors.error
: focused === 'email'
? colors.primary
: colors.textLight
}
/>
<TextInput
ref={emailRef}
placeholder="Email"
placeholderTextColor={colors.textLight}
autoCapitalize="none"
autoCorrect={false}
autoComplete="email"
keyboardType="email-address"
returnKeyType="next"
value={email}
onFocus={() => setFocused('email')}
onBlur={() => setFocused(null)}
onChangeText={handleEmailChange}
onSubmitEditing={() => passwordRef.current?.focus()}
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
/>
</Pressable>
{validationErrors.email && (
<Text style={[styles.errorText, { color: colors.error, fontFamily: fonts.regular }]}>
{validationErrors.email}
</Text>
)}
</View>
{/* Password input */}
<View>
<Pressable
style={[
styles.inputWrapper,
{
borderColor: validationErrors.password
? colors.error
: focused === 'password'
? colors.primary
: colors.border,
backgroundColor: colors.surface,
},
]}
onPress={() => passwordRef.current?.focus()}
>
<Icon
name="lock-outline"
size={20}
color={validationErrors.password
? colors.error
: focused === 'password'
? colors.primary
: colors.textLight
}
/>
<TextInput
ref={passwordRef}
placeholder="Password"
placeholderTextColor={colors.textLight}
secureTextEntry={!showPassword}
textContentType="password"
autoComplete="off"
autoCorrect={false}
autoCapitalize="none"
value={password}
onFocus={() => setFocused('password')}
onBlur={() => setFocused(null)}
onChangeText={handlePasswordChange}
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
/>
<TouchableOpacity
style={styles.iconButton}
onPress={() => setShowPassword(v => !v)}
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
accessibilityRole="button"
accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}
>
<Icon name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={22} color={colors.textLight} />
</TouchableOpacity>
</Pressable>
{validationErrors.password && (
<Text style={[styles.errorText, { color: colors.error, fontFamily: fonts.regular }]}>
{validationErrors.password}
</Text>
)}
</View>
{/* Row: Remember me + Forgot password */}
<View style={styles.rowBetween}>
<Pressable
style={styles.row}
onPress={() => {
setRememberMe(v => !v);
showInfo(rememberMe ? 'Will not remember login' : 'Will remember login');
}}
>
<Icon name={rememberMe ? 'checkbox-marked' : 'checkbox-blank-outline'} size={20} color={colors.primary} />
<Text style={[styles.rememberText, { color: colors.text, fontFamily: fonts.regular }]}>Remember me</Text>
</Pressable>
<TouchableOpacity onPress={() => showInfo('Forgot password feature coming soon!')}>
<Text style={[styles.link, { color: colors.primary, fontFamily: fonts.medium }]}>Forgot Password ?</Text>
</TouchableOpacity>
</View>
{/* Login button */}
<TouchableOpacity
activeOpacity={0.9}
onPress={handleLogin}
disabled={loading || !email.trim() || !password.trim()}
style={{ marginTop: 12 }}
>
<LinearGradient
colors={loading || !email.trim() || !password.trim()
? [colors.border, colors.border]
: ["#3AA0FF", "#2D6BFF"]
}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.loginButton}
>
<Text style={[styles.loginButtonText, { fontFamily: fonts.bold }]}>
{loading ? 'Logging in...' : 'Log In'}
</Text>
</LinearGradient>
</TouchableOpacity>
{/* Or divider */}
<View style={styles.orContainer}>
<View style={[styles.orLine, { backgroundColor: colors.border }]} />
<Text style={[styles.orText, { color: colors.textLight, fontFamily: fonts.regular }]}>Or login with</Text>
<View style={[styles.orLine, { backgroundColor: colors.border }]} />
</View>
{/* Social buttons */}
<View style={styles.socialRow}>
<TouchableOpacity
style={[styles.socialButton, { borderColor: colors.border }]}
onPress={() => showInfo('Google login coming soon!')}
>
<Icon name="google" size={20} color={colors.text} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.socialButton, { borderColor: colors.border }]}
onPress={() => showInfo('Facebook login coming soon!')}
>
<Icon name="facebook" size={20} color={colors.text} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.socialButton, { borderColor: colors.border }]}
onPress={() => showInfo('Apple login coming soon!')}
>
<Icon name="apple" size={20} color={colors.text} />
</TouchableOpacity>
</View>
{/* Sign up */}
<View style={styles.signupRow}>
<Text style={[styles.signupText, { color: colors.textLight, fontFamily: fonts.regular }]}>Don't have an account? </Text>
<TouchableOpacity onPress={() => navigation.navigate('Signup' as never)}>
<Text style={[styles.link, { color: colors.primary, fontFamily: fonts.medium }]}>Sign up</Text>
</TouchableOpacity>
</View>
{/* API Error */}
{!!error && (
<View style={styles.errorContainer}>
<Icon name="alert-circle-outline" size={16} color={colors.error} />
<Text style={[styles.errorText, { color: colors.error, fontFamily: fonts.regular }]}>
{error}
</Text>
</View>
)}
</View>
</View>
</GradientBackground>
);
};
const styles = StyleSheet.create({
gradient: {
flex: 1,
},
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 16,
},
card: {
width: '100%',
maxWidth: 380,
borderRadius: 16,
padding: 16,
borderWidth: 1,
},
logoCircle: {
alignSelf: 'center',
width: 48,
height: 48,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 8,
},
title: {
fontSize: 24,
textAlign: 'center',
},
subtitle: {
textAlign: 'center',
marginTop: 4,
marginBottom: 12,
},
inputWrapper: {
marginTop: 12,
borderWidth: 1,
borderRadius: 10,
paddingHorizontal: 12,
height: 52,
flexDirection: 'row',
alignItems: 'center',
},
input: {
paddingVertical: 0,
marginLeft: 8,
flex: 1,
},
iconButton: {
position: 'absolute',
right: 8,
top: 0,
bottom: 0,
width: 36,
alignItems: 'center',
justifyContent: 'center',
},
rowBetween: {
marginTop: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
rememberText: {
marginLeft: 6,
},
link: {},
loginButton: {
height: 48,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
loginButtonText: {
color: '#FFFFFF',
fontSize: 16,
},
orContainer: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 16,
},
orLine: {
flex: 1,
height: 1,
},
orText: {
marginHorizontal: 8,
fontSize: 12,
},
socialRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
socialButton: {
width: 52,
height: 44,
borderRadius: 8,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 6,
flex: 1,
},
signupRow: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 16,
},
signupText: {
fontSize: 12,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 12,
paddingHorizontal: 8,
},
errorText: {
marginLeft: 6,
fontSize: 14,
textAlign: 'left',
flex: 1,
},
});
export default LoginScreen;