EventBuzz_Checkin_App/src/screens/LoginScreen.tsx

276 lines
7.5 KiB
TypeScript

import React, { useState } from 'react';
import {
Image,
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StatusBar,
StyleSheet,
Text,
useWindowDimensions,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { authApi } from '../api/auth';
import type { RootStackParamList } from '../navigation/types';
import { useAuth } from '../context/AuthContext';
import { colors, radii, spacing, typography } from '../theme';
import { PrimaryButton, TextField } from '../components/ui';
type Props = NativeStackScreenProps<RootStackParamList, 'Login'>;
const BRAND_ORANGE = '#F7941D';
const BRAND_ORANGE_DARK = '#E8780A';
const BUTTON_HEIGHT = 52;
const BOTTOM_BAR_PAD_Y = 16;
export function LoginScreen({ navigation }: Props) {
const { refreshUser } = useAuth();
const { height: windowHeight } = useWindowDimensions();
const insets = useSafeAreaInsets();
const heroHeight = Math.round(windowHeight * 0.42);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPw, setShowPw] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const submit = async () => {
setError(null);
const em = email.trim();
if (!em || !password) {
setError('Email and password are required');
return;
}
setLoading(true);
let res;
try {
res = await authApi.login(em, password);
} catch {
setLoading(false);
setError('Network error. Check API URL and that the backend is running.');
return;
}
setLoading(false);
if (res.error) {
setError(res.error);
return;
}
const d = res.data;
if (d && 'requiresTwoFactor' in d && d.requiresTwoFactor) {
navigation.navigate('Login2FA', {
twoFactorToken: d.twoFactorToken,
email: d.email,
});
return;
}
await refreshUser();
navigation.replace('Events');
};
return (
<View style={styles.root}>
<StatusBar barStyle="light-content" backgroundColor={BRAND_ORANGE} />
{/* Orange hero — fixed pixel height so it always renders */}
<View style={[styles.hero, { height: heroHeight + insets.top, paddingTop: insets.top + 24 }]}>
<View style={styles.heroGradientDark} />
<View style={styles.heroCircleA} />
<View style={styles.heroCircleB} />
<View style={styles.logoCard}>
<Image
source={require('../assets/eventbuzz-logo.png')}
style={styles.logo}
resizeMode="contain"
/>
</View>
<Text style={styles.heroSubtitle}>Attendance Management</Text>
</View>
<KeyboardAvoidingView
style={styles.formArea}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.sheet}>
<ScrollView
keyboardShouldPersistTaps="handled"
contentContainerStyle={[
styles.sheetScroll,
{
paddingBottom:
BUTTON_HEIGHT +
BOTTOM_BAR_PAD_Y * 2 +
Math.max(insets.bottom, 16) +
18,
},
]}
showsVerticalScrollIndicator={false}
>
<Text style={styles.title}>Welcome back</Text>
<Text style={styles.sub}>Sign in to your dashboard account</Text>
{error ? (
<View style={styles.errorBox}>
<Text style={styles.errorText}>{error}</Text>
</View>
) : null}
<View style={styles.fields}>
<TextField
label="Email address"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
placeholder="you@example.com"
left={<Text style={styles.fieldIcon}>✉</Text>}
/>
<TextField
label="Password"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPw}
placeholder="Your password"
left={<Text style={styles.fieldIcon}>🔒</Text>}
right={
<Pressable onPress={() => setShowPw((v) => !v)} hitSlop={10}>
<Text style={styles.fieldIcon}>{showPw ? '🙈' : '👁'}</Text>
</Pressable>
}
/>
</View>
</ScrollView>
<View style={[styles.bottomBar, { paddingBottom: Math.max(insets.bottom, 16) }]}>
<PrimaryButton
title={loading ? 'Signing in…' : 'Sign In'}
loading={loading}
onPress={submit}
style={styles.signInBtn}
/>
</View>
</View>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: BRAND_ORANGE,
},
hero: {
backgroundColor: BRAND_ORANGE,
alignItems: 'center',
justifyContent: 'flex-end',
paddingBottom: 36,
overflow: 'hidden',
},
heroGradientDark: {
position: 'absolute',
right: 0,
bottom: 0,
width: '70%',
height: '80%',
backgroundColor: BRAND_ORANGE_DARK,
opacity: 0.45,
borderTopLeftRadius: 200,
},
heroCircleA: {
position: 'absolute',
width: 176,
height: 176,
borderRadius: 88,
right: -44,
top: -36,
backgroundColor: 'rgba(255,255,255,0.12)',
},
heroCircleB: {
position: 'absolute',
width: 128,
height: 128,
borderRadius: 64,
left: -28,
bottom: 8,
backgroundColor: 'rgba(255,255,255,0.10)',
},
logoCard: {
backgroundColor: '#fff',
borderRadius: 16,
paddingHorizontal: 20,
paddingVertical: 16,
marginBottom: 14,
zIndex: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.12,
shadowRadius: 20,
elevation: 8,
},
logo: { width: 144, height: 52 },
heroSubtitle: {
color: 'rgba(255,255,255,0.85)',
fontSize: 14,
letterSpacing: 0.4,
zIndex: 2,
},
formArea: {
flex: 1,
},
sheet: {
flex: 1,
marginTop: -28,
backgroundColor: '#fff',
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.08,
shadowRadius: 24,
elevation: 12,
overflow: 'hidden',
},
sheetScroll: {
paddingHorizontal: spacing.xl,
paddingTop: 32,
paddingBottom: 16,
},
title: { ...typography.title, color: colors.text, marginBottom: 6 },
sub: { ...typography.body, color: colors.muted, marginBottom: 24 },
fields: { gap: 16 },
fieldIcon: { fontSize: 16, color: '#9CA3AF' },
errorBox: {
backgroundColor: 'rgba(220,38,38,0.08)',
borderWidth: 1,
borderColor: 'rgba(220,38,38,0.18)',
borderRadius: radii.md,
paddingHorizontal: 14,
paddingVertical: 12,
marginBottom: 14,
},
errorText: { ...typography.body, color: colors.error },
signInBtn: {
borderRadius: 14,
shadowColor: BRAND_ORANGE,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 16,
elevation: 6,
},
bottomBar: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: spacing.xl,
paddingTop: BOTTOM_BAR_PAD_Y,
backgroundColor: '#fff',
},
});