Khởi tạo ban đầu

This commit is contained in:
Tran Anh Tuan
2025-11-28 16:59:57 +07:00
parent 2911be97b2
commit 4ba46a7df2
131 changed files with 28066 additions and 0 deletions

410
app/auth/login.tsx Normal file
View File

@@ -0,0 +1,410 @@
import EnIcon from "@/assets/icons/en_icon.png";
import VnIcon from "@/assets/icons/vi_icon.png";
import RotateSwitch from "@/components/rotate-switch";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import SliceSwitch from "@/components/ui/slice-switch";
import { TOKEN } from "@/constants";
import { Colors } from "@/constants/theme";
import { queryLogin } from "@/controller/AuthController";
import { useI18n } from "@/hooks/use-i18n";
import {
ColorScheme as ThemeColorScheme,
useTheme,
useThemeContext,
} from "@/hooks/use-theme-context";
import { showErrorToast } from "@/services/toast_service";
import {
getStorageItem,
removeStorageItem,
setStorageItem,
} from "@/utils/storage";
import { parseJwtToken } from "@/utils/token";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Image,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
export default function LoginScreen() {
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { t, setLocale, locale } = useI18n();
const { colors, colorScheme } = useTheme();
const { setThemeMode } = useThemeContext();
const styles = useMemo(
() => createStyles(colors, colorScheme),
[colors, colorScheme]
);
const placeholderColor = colors.textSecondary;
const buttonTextColor = colorScheme === "dark" ? colors.text : colors.surface;
const [isVNLang, setIsVNLang] = useState(false);
const checkLogin = useCallback(async () => {
const token = await getStorageItem(TOKEN);
if (!token) {
return;
}
const parsed = parseJwtToken(token);
const { exp } = parsed;
const now = Math.floor(Date.now() / 1000);
const oneHour = 60 * 60;
if (exp - now < oneHour) {
await removeStorageItem(TOKEN);
} else {
console.log("Token còn hạn");
router.replace("/(tabs)");
}
}, [router]);
useEffect(() => {
setIsVNLang(locale === "vi");
}, [locale]);
useEffect(() => {
checkLogin();
}, [checkLogin]);
const handleLogin = async (creds?: {
username: string;
password: string;
}) => {
const user = creds?.username ?? username;
const pass = creds?.password ?? password;
// Validate input
if (!user?.trim() || !pass?.trim()) {
showErrorToast("Vui lòng nhập tài khoản và mật khẩu");
return;
}
setLoading(true);
try {
const body: Model.LoginRequestBody = {
guid: "9812739812738213",
email: user,
password: pass,
};
const response = await queryLogin(body);
// Nếu thành công, lưu token và chuyển sang (tabs)
console.log("Login thành công với data:", response.data);
if (response?.data.token) {
// Lưu token vào storage nếu cần (thêm logic này sau)
console.log("Login Token ");
await setStorageItem(TOKEN, response.data.token);
// console.log("Token:", response.data.token);
router.replace("/(tabs)");
}
} catch (error) {
showErrorToast(
error instanceof Error ? error.message : "Đăng nhập thất bại"
);
} finally {
setLoading(false);
}
};
const handleSwitchLanguage = (isVN: boolean) => {
if (isVN) {
setLocale("vi");
} else {
setLocale("en");
}
setIsVNLang(isVN);
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<ScrollView
style={{ flex: 1, backgroundColor: colors.background }}
contentContainerStyle={styles.scrollContainer}
>
<ThemedView style={styles.container}>
{/* Header */}
<View style={styles.headerContainer}>
{/* Logo */}
<Image
source={require("@/assets/images/logo.png")}
style={styles.logo}
resizeMode="contain"
/>
<ThemedText type="title" style={styles.title}>
{t("common.app_name")}
</ThemedText>
</View>
{/* Form */}
<View style={styles.formContainer}>
{/* Username Input */}
<View style={styles.inputGroup}>
<ThemedText style={styles.label}>{t("auth.username")}</ThemedText>
<TextInput
style={styles.input}
placeholder={t("auth.username_placeholder")}
placeholderTextColor={placeholderColor}
value={username}
onChangeText={setUsername}
editable={!loading}
autoCapitalize="none"
/>
</View>
{/* Password Input */}
<View style={styles.inputGroup}>
<ThemedText style={styles.label}>{t("auth.password")}</ThemedText>
<View className="relative">
<TextInput
style={styles.input}
placeholder={t("auth.password_placeholder")}
placeholderTextColor={placeholderColor}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
editable={!loading}
autoCapitalize="none"
/>
{/* Position absolute with top:0 and bottom:0 and justifyContent:center
ensures the icon remains vertically centered inside the input */}
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={{
position: "absolute",
right: 12,
top: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
padding: 4,
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
<Ionicons
name={showPassword ? "eye-off" : "eye"}
size={22}
color={colors.icon}
/>
</TouchableOpacity>
</View>
</View>
{/* Login Button (3/4) + QR Scan (1/4) */}
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
}}
>
<TouchableOpacity
style={[
styles.loginButton,
loading && styles.loginButtonDisabled,
{ flex: 5, marginTop: 0 },
]}
onPress={() => handleLogin()}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={buttonTextColor} size="small" />
) : (
<Text
style={[styles.loginButtonText, { color: buttonTextColor }]}
>
{t("auth.login")}
</Text>
)}
</TouchableOpacity>
</View>
{/* Language Switcher */}
<View style={styles.languageSwitcherContainer}>
<RotateSwitch
initialValue={isVNLang}
onChange={handleSwitchLanguage}
size="sm"
offImage={EnIcon}
onImage={VnIcon}
/>
<SliceSwitch
size="sm"
leftIcon="moon"
leftIconColor={
colorScheme === "dark" ? colors.background : colors.surface
}
rightIcon="sunny"
rightIconColor={
colorScheme === "dark" ? colors.warning : "orange"
}
activeBackgroundColor={colors.text}
inactiveBackgroundColor={colors.surface}
inactiveOverlayColor={colors.textSecondary}
activeOverlayColor={colors.background}
value={colorScheme === "light"}
onChange={(val) => {
setThemeMode(val ? "light" : "dark");
}}
/>
</View>
{/* Copyright */}
<View style={styles.copyrightContainer}>
<ThemedText style={styles.copyrightText}>
© {new Date().getFullYear()} - {t("common.footer_text")}
</ThemedText>
</View>
</View>
</ThemedView>
</ScrollView>
</KeyboardAvoidingView>
);
}
const createStyles = (colors: typeof Colors.light, scheme: ThemeColorScheme) =>
StyleSheet.create({
scrollContainer: {
flexGrow: 1,
justifyContent: "center",
backgroundColor: colors.background,
},
container: {
flex: 1,
paddingHorizontal: 20,
justifyContent: "center",
},
headerContainer: {
marginBottom: 40,
alignItems: "center",
},
logo: {
width: 120,
height: 120,
marginBottom: 20,
},
title: {
fontSize: 28,
fontWeight: "bold",
marginBottom: 8,
},
subtitle: {
fontSize: 16,
opacity: 0.7,
},
formContainer: {
gap: 16,
},
inputGroup: {
gap: 8,
},
label: {
fontSize: 14,
fontWeight: "600",
},
input: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12,
fontSize: 16,
backgroundColor: colors.surface,
color: colors.text,
},
loginButton: {
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 8,
alignItems: "center",
marginTop: 16,
},
loginButtonDisabled: {
opacity: 0.6,
},
loginButtonText: {
fontSize: 16,
fontWeight: "600",
},
footerContainer: {
marginTop: 16,
alignItems: "center",
},
footerText: {
fontSize: 14,
},
linkText: {
color: colors.primary,
fontWeight: "600",
},
copyrightContainer: {
marginTop: 20,
alignItems: "center",
},
copyrightText: {
fontSize: 12,
opacity: 0.6,
textAlign: "center",
color: colors.textSecondary,
},
languageSwitcherContainer: {
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
marginTop: 24,
gap: 20,
},
languageSwitcherLabel: {
fontSize: 12,
fontWeight: "600",
textAlign: "center",
opacity: 0.8,
},
languageButtonsContainer: {
flexDirection: "row",
gap: 10,
},
languageButton: {
flex: 1,
paddingVertical: 10,
paddingHorizontal: 12,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
alignItems: "center",
backgroundColor: colors.surface,
},
languageButtonActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
languageButtonText: {
fontSize: 14,
fontWeight: "500",
color: colors.textSecondary,
},
languageButtonTextActive: {
color: scheme === "dark" ? colors.text : colors.surface,
fontWeight: "600",
},
});