* Extended functionalities
* Added different UIs
This commit is contained in:
10
meal-plan-frontend-mobile/app/(auth)/_layout.tsx
Normal file
10
meal-plan-frontend-mobile/app/(auth)/_layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
export default function AuthLayout() {
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="login" />
|
||||
<Stack.Screen name="register" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
117
meal-plan-frontend-mobile/app/(auth)/login.tsx
Normal file
117
meal-plan-frontend-mobile/app/(auth)/login.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Link, router } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/src/components/Button';
|
||||
import { Input } from '@/src/components/Input';
|
||||
import { Screen } from '@/src/components/Screen';
|
||||
import { useAuth } from '@/src/context/AuthContext';
|
||||
import { useAppearance } from '@/src/context/AppearanceContext';
|
||||
import { extractApiError } from '@/src/utils/apiError';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const { t } = useTranslation();
|
||||
const { login } = useAuth();
|
||||
const { colors } = useAppearance();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit() {
|
||||
setError(null);
|
||||
if (!email.trim()) {
|
||||
setError(t('validation.emailRequired'));
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
setError(t('validation.passwordRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await login({ email: email.trim(), password });
|
||||
router.replace('/(main)/(tabs)');
|
||||
} catch (err) {
|
||||
setError(extractApiError(err, t('errors.signInFailed')));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
|
||||
<Text style={[styles.brand, { color: colors.brand }]}>{t('app.name')}</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textMuted }]}>{t('auth.signInSubtitle')}</Text>
|
||||
|
||||
<View style={styles.form}>
|
||||
<Input
|
||||
label={t('auth.email')}
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
placeholder={t('auth.emailPlaceholder')}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth.password')}
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
/>
|
||||
{error ? <Text style={[styles.error, { color: colors.danger }]}>{error}</Text> : null}
|
||||
<Button
|
||||
title={loading ? t('auth.signingIn') : t('auth.signIn')}
|
||||
loading={loading}
|
||||
onPress={handleSubmit}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.footer, { color: colors.textMuted }]}>
|
||||
{t('auth.noAccount')}{' '}
|
||||
<Link href="/(auth)/register" style={{ color: colors.brand, fontWeight: '600' }}>
|
||||
{t('auth.createOne')}
|
||||
</Link>
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
scroll: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
paddingVertical: 24,
|
||||
},
|
||||
brand: {
|
||||
fontSize: 34,
|
||||
fontWeight: '800',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
form: {
|
||||
gap: 14,
|
||||
marginTop: 8,
|
||||
},
|
||||
error: {
|
||||
fontSize: 14,
|
||||
},
|
||||
footer: {
|
||||
textAlign: 'center',
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
145
meal-plan-frontend-mobile/app/(auth)/register.tsx
Normal file
145
meal-plan-frontend-mobile/app/(auth)/register.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Link, router } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/src/components/Button';
|
||||
import { Input } from '@/src/components/Input';
|
||||
import { Screen } from '@/src/components/Screen';
|
||||
import { useAuth } from '@/src/context/AuthContext';
|
||||
import { useAppearance } from '@/src/context/AppearanceContext';
|
||||
import { extractApiError } from '@/src/utils/apiError';
|
||||
|
||||
function validatePassword(password: string, t: (key: string) => string): string | null {
|
||||
if (password.length < 8) return t('validation.passwordMinLength');
|
||||
if (!/[A-Z]/.test(password)) return t('validation.passwordUppercase');
|
||||
if (!/[a-z]/.test(password)) return t('validation.passwordLowercase');
|
||||
if (!/\d/.test(password)) return t('validation.passwordDigit');
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function RegisterScreen() {
|
||||
const { t } = useTranslation();
|
||||
const { register } = useAuth();
|
||||
const { colors } = useAppearance();
|
||||
const [email, setEmail] = useState('');
|
||||
const [userName, setUserName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit() {
|
||||
setError(null);
|
||||
if (!email.trim()) {
|
||||
setError(t('validation.emailRequired'));
|
||||
return;
|
||||
}
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
||||
setError(t('validation.emailInvalid'));
|
||||
return;
|
||||
}
|
||||
const passwordError = validatePassword(password, t);
|
||||
if (passwordError) {
|
||||
setError(passwordError);
|
||||
return;
|
||||
}
|
||||
if (!confirmPassword) {
|
||||
setError(t('validation.confirmPasswordRequired'));
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
setError(t('validation.passwordsMismatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await register({
|
||||
email: email.trim(),
|
||||
password,
|
||||
userName: userName.trim() || undefined,
|
||||
});
|
||||
router.replace('/(main)/(tabs)');
|
||||
} catch (err) {
|
||||
setError(extractApiError(err, t('errors.registerFailed')));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen title={t('auth.createAccountTitle')} subtitle={t('auth.createAccountSubtitle')}>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
|
||||
<View style={styles.form}>
|
||||
<Input
|
||||
label={t('auth.email')}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
placeholder={t('auth.emailPlaceholder')}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth.displayName')}
|
||||
placeholder={t('auth.displayNamePlaceholder')}
|
||||
value={userName}
|
||||
onChangeText={setUserName}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth.password')}
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
/>
|
||||
<Text style={[styles.hint, { color: colors.textMuted }]}>{t('auth.passwordHint')}</Text>
|
||||
<Input
|
||||
label={t('auth.confirmPassword')}
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
/>
|
||||
{error ? <Text style={[styles.error, { color: colors.danger }]}>{error}</Text> : null}
|
||||
<Button
|
||||
title={loading ? t('auth.creatingAccount') : t('auth.createAccount')}
|
||||
loading={loading}
|
||||
onPress={handleSubmit}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.footer, { color: colors.textMuted }]}>
|
||||
{t('auth.hasAccount')}{' '}
|
||||
<Link href="/(auth)/login" style={{ color: colors.brand, fontWeight: '600' }}>
|
||||
{t('auth.signIn')}
|
||||
</Link>
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
scroll: {
|
||||
gap: 16,
|
||||
paddingBottom: 24,
|
||||
},
|
||||
form: {
|
||||
gap: 14,
|
||||
},
|
||||
hint: {
|
||||
fontSize: 13,
|
||||
marginTop: -6,
|
||||
},
|
||||
error: {
|
||||
fontSize: 14,
|
||||
},
|
||||
footer: {
|
||||
textAlign: 'center',
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user