feat: 登录+忘记密码
This commit is contained in:
parent
3d4ee1b210
commit
727ebcb483
4
app.json
4
app.json
@ -50,7 +50,9 @@
|
||||
"expo-font",
|
||||
{
|
||||
"fonts": [
|
||||
"./assets/font/english.otf"
|
||||
"./assets/fonts/Quicksand.otf",
|
||||
"./assets/fonts/SF-Pro.otf",
|
||||
"./assets/fonts/Inter-Regular.otf"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@ -60,7 +60,9 @@ export default function TabLayout() {
|
||||
|
||||
// 加载字体
|
||||
const [loaded, error] = useFonts({
|
||||
english: require('@/assets/font/english.otf'),
|
||||
quicksand: require('@/assets/fonts/Quicksand.otf'),
|
||||
sfPro: require('@/assets/fonts/SF-Pro.otf'),
|
||||
inter: require('@/assets/fonts/Inter-Regular.otf'),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -74,9 +74,9 @@ const LoginScreen = () => {
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bounces={false}
|
||||
>
|
||||
<ThemedView className="flex-1 bg-bgPrimary justify-end">
|
||||
<ThemedView className="flex-1 justify-end" bgColor="bgPrimary">
|
||||
<View style={{ width: "100%", alignItems: "center", marginTop: insets.top + 8, opacity: keyboardOffset === 0 ? 1 : 0 }}>
|
||||
<ThemedText style={{ fontSize: 20, fontWeight: 'bold', color: "#fff" }}>{t('login:auth.login.titleText')}</ThemedText>
|
||||
<ThemedText size="xl" weight="bold" color="textWhite">{t('login:auth.login.titleText')}</ThemedText>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<View
|
||||
@ -105,7 +105,8 @@ const LoginScreen = () => {
|
||||
</View>
|
||||
</View>
|
||||
<ThemedView
|
||||
className="w-full bg-white pt-4 px-6 relative z-20 shadow-lg pb-5"
|
||||
className="w-full pt-4 px-6 relative z-20 shadow-lg pb-5"
|
||||
bgColor="textWhite"
|
||||
style={{
|
||||
borderTopLeftRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
@ -120,7 +121,7 @@ const LoginScreen = () => {
|
||||
>
|
||||
{/* 错误提示 */}
|
||||
<View className={`${error !== "123" ? 'opacity-100' : 'opacity-0'} w-full flex justify-center items-center text-primary-500 text-sm`}>
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
<ThemedText size='xxs' color='bgSecondary' type='inter'>
|
||||
{error}
|
||||
</ThemedText>
|
||||
</View>
|
||||
@ -161,22 +162,22 @@ const LoginScreen = () => {
|
||||
return components[status as keyof typeof components] || components.login;
|
||||
})()}
|
||||
|
||||
<View style={{ width: "100%", alignItems: "center", marginTop: 16 }}>
|
||||
<View style={{ width: "100%", alignItems: "center", }}>
|
||||
{status == 'login' || !status &&
|
||||
<View className="flex-row justify-center mt-2 flex-wrap w-[85%] items-center">
|
||||
<ThemedText style={{ fontSize: 11, color: "#FFB645" }}>
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter'>
|
||||
{status === 'login' || !status ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('terms') }}>
|
||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", textDecorationLine: 'underline' }}>
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter' style={{ textDecorationLine: 'underline' }}>
|
||||
{t('auth.agree.terms', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", flexWrap: 'wrap' }}>
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap'>
|
||||
{t('auth.agree.join', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('privacy') }}>
|
||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", textDecorationLine: 'underline' }}>
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap' style={{ textDecorationLine: 'underline' }}>
|
||||
{t('auth.agree.privacyPolicy', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
BIN
assets/fonts/Inter-Regular.otf
Normal file
BIN
assets/fonts/Inter-Regular.otf
Normal file
Binary file not shown.
BIN
assets/fonts/SF-Pro.otf
Normal file
BIN
assets/fonts/SF-Pro.otf
Normal file
Binary file not shown.
@ -1,16 +1,26 @@
|
||||
import { StyleProp, StyleSheet, Text, TextStyle, type TextProps } from 'react-native';
|
||||
|
||||
import { Fonts } from '@/constants/Fonts';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { FontColor, Fonts } from '@/constants/Fonts';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
|
||||
export type ThemeColor = keyof typeof Colors.light & keyof typeof Colors.dark;
|
||||
export type ColorValue = `#${string}` | `rgb(${string})` | `rgba(${string})` | string;
|
||||
|
||||
export type ThemedTextProps = TextProps & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
|
||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'sfPro' | 'inter';
|
||||
weight?: 'regular' | 'medium' | 'semiBold' | 'bold';
|
||||
size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
size?: 'xxs' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
radius?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
color?: ThemeColor | FontColor | ColorValue;
|
||||
};
|
||||
|
||||
export function isFontColorKey(key: string): key is FontColor {
|
||||
return ['bgPrimary', 'bgSecondary', 'textPrimary', 'textSecondary', 'textThird', 'textWhite'].includes(key);
|
||||
}
|
||||
|
||||
export function ThemedText({
|
||||
style,
|
||||
lightColor,
|
||||
@ -18,20 +28,38 @@ export function ThemedText({
|
||||
type = 'default',
|
||||
weight = 'regular',
|
||||
size,
|
||||
radius,
|
||||
color,
|
||||
...rest
|
||||
}: ThemedTextProps) {
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
const themeColor = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
const textColor = (() => {
|
||||
if (!color) return themeColor;
|
||||
|
||||
// 检查是否是主题颜色
|
||||
const themeColors = Object.keys(Colors.light) as ThemeColor[];
|
||||
if (themeColors.includes(color as ThemeColor)) {
|
||||
return useThemeColor({ light: lightColor, dark: darkColor }, color as ThemeColor);
|
||||
}
|
||||
|
||||
// 检查是否是 Fonts 中定义的颜色
|
||||
if (isFontColorKey(color)) {
|
||||
return Fonts[color as FontColor];
|
||||
}
|
||||
|
||||
// 返回自定义颜色值
|
||||
return color;
|
||||
})();
|
||||
|
||||
|
||||
const baseStyle: StyleProp<TextStyle> = {
|
||||
fontFamily: Fonts.primary,
|
||||
color,
|
||||
fontWeight: Fonts[weight as keyof typeof Fonts] as TextStyle['fontWeight'],
|
||||
fontFamily: Fonts.quicksand,
|
||||
color: textColor,
|
||||
fontWeight: Number(Fonts[weight as keyof typeof Fonts]) as TextStyle['fontWeight'],
|
||||
};
|
||||
|
||||
if (size) {
|
||||
baseStyle.fontSize = Fonts[size as keyof typeof Fonts];
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
@ -41,6 +69,11 @@ export function ThemedText({
|
||||
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
||||
type === 'subtitle' ? styles.subtitle : undefined,
|
||||
type === 'link' ? styles.link : undefined,
|
||||
type === 'sfPro' ? styles.sfPro : undefined,
|
||||
type === 'inter' ? styles.inter : undefined,
|
||||
size && { fontSize: Number(Fonts[size as keyof typeof Fonts]) },
|
||||
weight && { fontWeight: Number(Fonts[weight as keyof typeof Fonts]) as TextStyle['fontWeight'] },
|
||||
color && { color: textColor },
|
||||
style,
|
||||
]}
|
||||
{...rest}
|
||||
@ -50,11 +83,12 @@ export function ThemedText({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
default: {
|
||||
fontSize: Fonts.base,
|
||||
fontSize: Number(Fonts.base),
|
||||
lineHeight: 24,
|
||||
fontFamily: Fonts.quicksand,
|
||||
},
|
||||
defaultSemiBold: {
|
||||
fontSize: Fonts.base,
|
||||
fontSize: Number(Fonts.base),
|
||||
lineHeight: 24,
|
||||
fontWeight: '600',
|
||||
},
|
||||
@ -74,4 +108,16 @@ const styles = StyleSheet.create({
|
||||
color: '#0a7ea4',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
sfPro: {
|
||||
fontSize: Number(Fonts.base),
|
||||
lineHeight: 24,
|
||||
fontWeight: '600',
|
||||
fontFamily: Fonts.sfPro,
|
||||
},
|
||||
inter: {
|
||||
fontSize: Number(Fonts.base),
|
||||
lineHeight: 24,
|
||||
fontWeight: '600',
|
||||
fontFamily: Fonts.inter,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,9 +1,32 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { FontColor, Fonts } from '@/constants/Fonts';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
import { View, type ViewProps } from 'react-native';
|
||||
import { ColorValue, isFontColorKey, ThemeColor } from './ThemedText';
|
||||
|
||||
type ThemedViewProps = ViewProps & {
|
||||
className?: string;
|
||||
bgColor?: FontColor | ColorValue | string;
|
||||
};
|
||||
|
||||
export function ThemedView({ className, style, ...props }: ThemedViewProps) {
|
||||
return <View className={className} style={style} {...props} />;
|
||||
export function ThemedView({ className, style, bgColor, ...props }: ThemedViewProps) {
|
||||
const themeColor = useThemeColor({ light: bgColor, dark: bgColor }, 'background');
|
||||
|
||||
const bgColorValue = (() => {
|
||||
if (!bgColor) return themeColor;
|
||||
|
||||
// 检查是否是主题颜色
|
||||
const themeColors = Object.keys(Colors.light) as ThemeColor[];
|
||||
if (themeColors.includes(bgColor as ThemeColor)) {
|
||||
return useThemeColor({ light: bgColor, dark: bgColor }, bgColor as ThemeColor);
|
||||
}
|
||||
// 检查是否是 Fonts 中定义的颜色
|
||||
if (isFontColorKey(bgColor)) {
|
||||
return Fonts[bgColor];
|
||||
}
|
||||
// 返回自定义颜色值
|
||||
return bgColor;
|
||||
})();
|
||||
|
||||
return <View className={className} style={[{ backgroundColor: bgColorValue }, style]} {...props} />;
|
||||
}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { User } from "@/types/user";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
import Button from "./ui/Button";
|
||||
import TextInput from "./ui/TextInput";
|
||||
|
||||
interface LoginProps {
|
||||
setIsSignUp?: (isSignUp: string) => void;
|
||||
@ -69,45 +72,29 @@ const ForgetPwd = ({ setIsSignUp, updateUrlParam, setError }: LoginProps) => {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.forgetPwd.title', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.submitButton,
|
||||
(isDisabled || loading) && styles.disabledButton
|
||||
]}
|
||||
onPress={handleSubmit}
|
||||
disabled={isDisabled || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText style={styles.buttonText}>
|
||||
{isDisabled
|
||||
? `${t("auth.forgetPwd.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)`
|
||||
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{/* 邮箱 */}
|
||||
<TextInput
|
||||
label={t('auth.forgetPwd.title', { ns: 'login' })}
|
||||
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
value={email}
|
||||
/>
|
||||
{/* 发送邮箱 */}
|
||||
<Button
|
||||
isLoading={isDisabled || loading}
|
||||
handleLogin={handleSubmit}
|
||||
text={isDisabled
|
||||
? `${t("auth.forgetPwd.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)`
|
||||
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
||||
/>
|
||||
|
||||
{/* 返回登录 */}
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={handleBackToLogin}
|
||||
>
|
||||
<ThemedText style={styles.backButtonText}>
|
||||
<ThemedText type='inter' color="bgSecondary" size="sm">
|
||||
{t('auth.forgetPwd.goback', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
@ -123,16 +110,24 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 16,
|
||||
color: '#1F2937',
|
||||
fontSize: Fonts['base'],
|
||||
color: Fonts['textPrimary'],
|
||||
fontWeight: Fonts['bold'],
|
||||
fontFamily: Fonts['sfPro'],
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
textInput: {
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
backgroundColor: '#FFF8DE',
|
||||
borderRadius: Fonts['xs'],
|
||||
paddingHorizontal: Fonts['base'],
|
||||
paddingVertical: Fonts['xs'],
|
||||
fontSize: Fonts['sm'],
|
||||
lineHeight: Fonts['base'],
|
||||
textAlignVertical: 'center',
|
||||
backgroundColor: Fonts['bgInput'],
|
||||
color: Fonts['textSecondary'],
|
||||
fontFamily: Fonts['inter'],
|
||||
paddingRight: Fonts['5xl'],
|
||||
},
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
@ -151,11 +146,7 @@ const styles = StyleSheet.create({
|
||||
backButton: {
|
||||
alignSelf: 'center',
|
||||
marginTop: 24,
|
||||
},
|
||||
backButtonText: {
|
||||
color: '#1F2937',
|
||||
fontSize: 14,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default ForgetPwd;
|
||||
@ -1,14 +1,15 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
import { useAuth } from "../../contexts/auth-context";
|
||||
import { fetchApi } from "../../lib/server-api-util";
|
||||
import { User } from "../../types/user";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
import Button from "./ui/Button";
|
||||
import TextInput from "./ui/TextInput";
|
||||
|
||||
const REMEMBER_ACCOUNT_KEY = 'fairclip_remembered_account';
|
||||
interface LoginProps {
|
||||
updateUrlParam: (status: string, value: string) => void;
|
||||
setError: (error: string) => void;
|
||||
@ -22,7 +23,6 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email) {
|
||||
@ -69,85 +69,61 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.inputContainer, { marginBottom: 20 }]}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.login.email', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={email}
|
||||
onChangeText={(text) => {
|
||||
setEmail(text);
|
||||
setError('123');
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.login.password', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<View style={styles.passwordInputContainer}>
|
||||
<TextInput
|
||||
style={[styles.textInput, { paddingRight: 48 }]}
|
||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={password}
|
||||
onChangeText={(text) => {
|
||||
setPassword(text);
|
||||
setError('123');
|
||||
}}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye' : 'eye-off'}
|
||||
size={20}
|
||||
color="#666"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{/* 邮箱 */}
|
||||
<TextInput
|
||||
label={t('auth.login.email', { ns: 'login' })}
|
||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||
onChangeText={(text) => {
|
||||
setEmail(text);
|
||||
setError('123');
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
value={email}
|
||||
/>
|
||||
|
||||
{/* 密码 */}
|
||||
<TextInput
|
||||
label={t('auth.login.password', { ns: 'login' })}
|
||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||
onChangeText={(text) => {
|
||||
setPassword(text);
|
||||
setError('123');
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
value={password}
|
||||
type="password"
|
||||
setShowPassword={setShowPassword}
|
||||
showPassword={showPassword}
|
||||
containerStyle={{ marginBottom: 0 }}
|
||||
/>
|
||||
|
||||
{/* 忘记密码 */}
|
||||
<TouchableOpacity
|
||||
style={styles.forgotPassword}
|
||||
onPress={handleForgotPassword}
|
||||
>
|
||||
<ThemedText style={styles.forgotPasswordText}>
|
||||
<ThemedText style={styles.forgotPasswordText} color="textPrimary" type="inter">
|
||||
{t('auth.login.forgotPassword', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.loginButton, isLoading && { opacity: 0.7 }]}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText style={styles.loginButtonText}>
|
||||
{t('auth.login.loginButton', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{/* 登录按钮 */}
|
||||
<Button isLoading={isLoading} handleLogin={handleLogin} text={t('auth.login.loginButton', { ns: 'login' })} />
|
||||
|
||||
{/* 注册 */}
|
||||
<View style={styles.signupContainer}>
|
||||
<ThemedText style={styles.signupText}>
|
||||
<ThemedText style={styles.signupText} type="sfPro">
|
||||
{t('auth.login.signUpMessage', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={handleSignUp}>
|
||||
<ThemedText style={styles.signupLink}>
|
||||
<ThemedText style={styles.signupLink} type="sfPro">
|
||||
{t('auth.login.signUp', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 第三方登录 */}
|
||||
<View style={{ width: "100%", alignItems: "center", opacity: 0 }}>
|
||||
<View style={styles.loginTypeContainer}>
|
||||
<ThemedText>
|
||||
@ -178,8 +154,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
loginType: {
|
||||
borderRadius: 12,
|
||||
width: 54,
|
||||
height: 54,
|
||||
width: 42,
|
||||
height: 42,
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#FADBA1'
|
||||
},
|
||||
@ -187,19 +163,24 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 16,
|
||||
color: '#AC7E35',
|
||||
fontWeight: '600',
|
||||
fontSize: Fonts['base'],
|
||||
color: Fonts['textPrimary'],
|
||||
fontWeight: Fonts['bold'],
|
||||
fontFamily: Fonts['sfPro'],
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
textInput: {
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 14,
|
||||
borderRadius: Fonts['xs'],
|
||||
paddingHorizontal: Fonts['base'],
|
||||
paddingVertical: Fonts['xs'],
|
||||
fontSize: Fonts['sm'],
|
||||
lineHeight: Fonts['base'],
|
||||
textAlignVertical: 'center',
|
||||
backgroundColor: '#FFF8DE'
|
||||
backgroundColor: Fonts['bgInput'],
|
||||
color: Fonts['textSecondary'],
|
||||
fontFamily: Fonts['inter'],
|
||||
paddingRight: Fonts['5xl'],
|
||||
},
|
||||
passwordInputContainer: {
|
||||
position: 'relative',
|
||||
@ -237,11 +218,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
signupText: {
|
||||
color: '#AC7E35',
|
||||
fontSize: 17,
|
||||
fontSize: Fonts['sm'],
|
||||
},
|
||||
signupLink: {
|
||||
color: '#E2793F',
|
||||
fontSize: 17,
|
||||
fontSize: Fonts['sm'],
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
textDecorationLine: 'underline',
|
||||
|
||||
37
components/login/ui/Button.tsx
Normal file
37
components/login/ui/Button.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ActivityIndicator, StyleSheet, TouchableOpacity, ViewStyle } from "react-native";
|
||||
|
||||
interface ButtonProps {
|
||||
isLoading?: boolean;
|
||||
handleLogin?: () => void;
|
||||
text: string;
|
||||
containerStyle?: ViewStyle;
|
||||
}
|
||||
const Button = ({ isLoading, handleLogin, text, containerStyle }: ButtonProps) => {
|
||||
return <TouchableOpacity
|
||||
style={[styles.loginButton, isLoading && { opacity: 0.7 }, containerStyle]}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText type="sfPro" size="lg" weight="bold" color="textWhite">
|
||||
{text}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loginButton: {
|
||||
width: '100%',
|
||||
backgroundColor: '#E2793F',
|
||||
borderRadius: 28,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
})
|
||||
|
||||
export default Button
|
||||
111
components/login/ui/TextInput.tsx
Normal file
111
components/login/ui/TextInput.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { TextInput as RNTextInput, TextInputProps as RNTextInputProps, StyleProp, StyleSheet, TextStyle, TouchableOpacity, View, ViewStyle } from "react-native";
|
||||
|
||||
interface CustomTextInputProps {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
showPassword?: boolean;
|
||||
setShowPassword?: (showPassword: boolean) => void;
|
||||
setError?: (error: string) => void;
|
||||
type?: 'default' | 'password';
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
type TextInputProps = RNTextInputProps & CustomTextInputProps;
|
||||
|
||||
const TextInput = ({
|
||||
type = 'default',
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
onChangeText,
|
||||
setError,
|
||||
showPassword,
|
||||
setShowPassword,
|
||||
style,
|
||||
containerStyle,
|
||||
...props
|
||||
}: TextInputProps) => {
|
||||
|
||||
return (
|
||||
<View style={[styles.inputContainer, containerStyle]}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{label}
|
||||
</ThemedText>
|
||||
<View style={styles.inputTextContainer}>
|
||||
<RNTextInput
|
||||
style={[styles.textInput, style]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={Fonts['placeholderTextColor']}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
secureTextEntry={type === 'password' ? !showPassword : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{
|
||||
type === 'password' &&
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => {
|
||||
if (setShowPassword) {
|
||||
setShowPassword(!showPassword);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye' : 'eye-off'}
|
||||
size={20}
|
||||
color="#666"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: Fonts['sm'],
|
||||
color: Fonts['textPrimary'],
|
||||
fontWeight: Fonts['bold'],
|
||||
fontFamily: Fonts['sfPro'],
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
textInput: {
|
||||
borderRadius: Fonts['xs'],
|
||||
paddingHorizontal: Fonts['base'],
|
||||
paddingVertical: Fonts['sm'],
|
||||
fontSize: Fonts['sm'],
|
||||
lineHeight: Fonts['base'],
|
||||
textAlignVertical: 'center',
|
||||
backgroundColor: Fonts['bgInput'],
|
||||
color: Fonts['textSecondary'],
|
||||
fontFamily: Fonts['inter'],
|
||||
paddingRight: 48, // Make space for the eye icon
|
||||
},
|
||||
inputTextContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
eyeIcon: {
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
top: '50%',
|
||||
transform: [{ translateY: -10 }], // Half of the icon's height (20/2 = 10)
|
||||
height: 20,
|
||||
width: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
export default TextInput;
|
||||
@ -1,14 +1,18 @@
|
||||
export const Fonts = {
|
||||
// Font family
|
||||
primary: 'english',
|
||||
quicksand: 'quicksand',
|
||||
sfPro: 'sfPro',
|
||||
inter: 'inter',
|
||||
|
||||
// Font weights
|
||||
regular: '400',
|
||||
medium: '500',
|
||||
semiBold: '600',
|
||||
bold: '700',
|
||||
extraBold: '800',
|
||||
|
||||
// Font sizes
|
||||
xxs: 11,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
base: 16,
|
||||
@ -18,7 +22,20 @@ export const Fonts = {
|
||||
'3xl': 30,
|
||||
'4xl': 36,
|
||||
'5xl': 48,
|
||||
|
||||
// color
|
||||
bgPrimary: '#FFB645',
|
||||
bgSecondary: '#E2793F',
|
||||
bgInput: '#FFF8DE',
|
||||
textPrimary: '#AC7E35',
|
||||
textSecondary: '#4C320C',
|
||||
textThird: '#7F786F',
|
||||
textWhite: "#FFFFFF",
|
||||
placeholderTextColor: "#ccc",
|
||||
|
||||
} as const;
|
||||
|
||||
export type FontWeight = keyof Omit<typeof Fonts, 'primary' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'>;
|
||||
export type FontWeight = keyof Omit<typeof Fonts, 'quicksand' | 'sfPro' | 'inter' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'>;
|
||||
export type FontSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
export type FontColor = 'bgPrimary' | 'bgSecondary' | 'textPrimary' | 'textSecondary' | 'textThird' | 'textWhite';
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user