chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,280 @@
import React, { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
ActivityIndicator,
StyleSheet,
} from "react-native";
import { Feather, FontAwesome } from "@expo/vector-icons";
import { loginUser } from "../../api/api";
import { useAuth } from "../../auth/AuthContext";
import type { LoginRequest } from "../../api/api_types";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { RootStackParamList } from "../../navigation/types";
import { useTheme } from "../../context/ThemeContext";
type LoginScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
"login"
>;
const LoginClient = () => {
const navigation = useNavigation<LoginScreenNavigationProp>();
const { loginClient } = useAuth();
const { colors } = useTheme();
const [formData, setFormData] = useState<LoginRequest>({
username: "",
password: "",
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{
username?: string;
password?: string;
}>({});
const [apiError, setApiError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const validateForm = () => {
const newErrors: { username?: string; password?: string } = {};
if (!formData.username.trim()) newErrors.username = "Username requis";
else if (formData.username.trim().length < 3)
newErrors.username = "Username trop court";
if (!formData.password) newErrors.password = "Mot de passe requis";
else if (formData.password.length < 6)
newErrors.password = "Mot de passe trop court";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validateForm()) return;
setIsLoading(true);
setApiError("");
try {
const result = await loginUser(
formData.username,
formData.password,
);
if (result.success && result.access_token) {
await loginClient(result.access_token);
} else {
const errorMessage =
result.message || "Identifiants incorrects";
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
} catch (err) {
const message =
err instanceof Error ? err.message : "Erreur de connexion";
setApiError(message);
setErrors({ username: message });
} finally {
setIsLoading(false);
}
};
const handleChange = (name: "username" | "password", value: string) => {
setFormData((prev) => ({ ...prev, [name]: value }));
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: undefined }));
if (apiError) setApiError("");
};
return (
<View
style={[styles.container, { backgroundColor: colors.bgSecondary }]}
>
<View style={[styles.card, { backgroundColor: colors.bgPrimary }]}>
<View style={styles.header}>
<FontAwesome name="user-circle" size={48} color="#7c3aed" />
<Text style={[styles.title, { color: colors.textWhite }]}>
Connexion Client
</Text>
<Text
style={[styles.subtitle, { color: colors.textMuted }]}
>
Accédez à votre espace personnel
</Text>
</View>
{apiError ? (
<Text style={styles.apiError}> {apiError}</Text>
) : null}
<View style={styles.inputGroup}>
<Text
style={[styles.label, { color: colors.textSecondary }]}
>
Username
</Text>
<View
style={[
styles.inputWrapper,
{
borderColor: colors.border,
backgroundColor: colors.bgInput,
},
]}
>
<Feather
name="user"
size={20}
color={colors.textMuted}
style={styles.icon}
/>
<TextInput
style={[
styles.input,
{ color: colors.textWhite },
errors.username && styles.inputError,
]}
placeholder="Votre username"
placeholderTextColor={colors.textMuted}
value={formData.username}
onChangeText={(value) =>
handleChange("username", value)
}
editable={!isLoading}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
{errors.username && (
<Text style={styles.errorText}>{errors.username}</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text
style={[styles.label, { color: colors.textSecondary }]}
>
Mot de passe
</Text>
<View
style={[
styles.inputWrapper,
{
borderColor: colors.border,
backgroundColor: colors.bgInput,
},
]}
>
<Feather
name="lock"
size={20}
color={colors.textMuted}
style={styles.icon}
/>
<TextInput
style={[
styles.input,
{ color: colors.textWhite },
errors.password && styles.inputError,
]}
placeholder="••••••"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showPassword}
value={formData.password}
onChangeText={(value) =>
handleChange("password", value)
}
editable={!isLoading}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeButton}
onPress={() => setShowPassword(!showPassword)}
>
<Feather
name={showPassword ? "eye-off" : "eye"}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
{errors.password && (
<Text style={styles.errorText}>{errors.password}</Text>
)}
</View>
<TouchableOpacity
style={[styles.submitButton, isLoading && { opacity: 0.6 }]}
onPress={handleSubmit}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.submitText}>Se connecter</Text>
)}
</TouchableOpacity>
<View style={styles.signup}>
<Text
style={[styles.signupText, { color: colors.textMuted }]}
>
Pas encore de compte ?{" "}
<Text
style={styles.signupLink}
onPress={() => navigation.navigate("register")}
>
Créer un compte
</Text>
</Text>
</View>
</View>
</View>
);
};
export default LoginClient;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
card: { width: "100%", maxWidth: 400, borderRadius: 16, padding: 24 },
header: { alignItems: "center", marginBottom: 24 },
title: { fontSize: 24, fontWeight: "bold", marginTop: 8 },
subtitle: { fontSize: 14, marginTop: 4, textAlign: "center" },
apiError: {
backgroundColor: "#fee2e2",
color: "#991b1b",
padding: 12,
borderRadius: 6,
marginBottom: 16,
textAlign: "center",
},
inputGroup: { marginBottom: 16 },
label: { marginBottom: 4, fontWeight: "500" },
inputWrapper: {
flexDirection: "row",
alignItems: "center",
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 12,
},
icon: { marginRight: 8 },
input: { flex: 1, height: 40 },
inputError: { borderColor: "#ef4444" },
errorText: { color: "#ef4444", fontSize: 12, marginTop: 4 },
eyeButton: { padding: 4 },
submitButton: {
backgroundColor: "#7c3aed",
borderRadius: 8,
padding: 12,
alignItems: "center",
marginTop: 8,
},
submitText: { color: "white", fontWeight: "600", fontSize: 16 },
signup: { marginTop: 16, alignItems: "center" },
signupText: {},
signupLink: { color: "#a78bfa", fontWeight: "600" },
});
+328
View File
@@ -0,0 +1,328 @@
import React, { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ScrollView,
ActivityIndicator,
} from "react-native";
import { Feather, FontAwesome } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { RootStackParamList } from "../../navigation/types";
import { registerUser } from "../../api/api";
import { useAuth } from "../../auth/AuthContext";
import { useTheme } from "../../context/ThemeContext";
type RegisterScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
"register"
>;
const RegisterScreen = () => {
const navigation = useNavigation<RegisterScreenNavigationProp>();
const { loginClient } = useAuth();
const { colors } = useTheme();
const [formData, setFormData] = useState({
nom: "",
prenom: "",
telephone: "",
username: "",
password: "",
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [apiError, setApiError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleChange = (name: string, value: string) => {
setFormData((prev) => ({ ...prev, [name]: value }));
setErrors((prev) => ({ ...prev, [name]: undefined }));
if (apiError) setApiError("");
};
const validateForm = () => {
const newErrors: typeof errors = {};
if (!formData.nom.trim()) newErrors.nom = "Le nom est requis";
if (!formData.prenom.trim()) newErrors.prenom = "Le prénom est requis";
const cleanPhone = formData.telephone.replace(/\s/g, "");
if (!formData.telephone.trim())
newErrors.telephone = "Le numéro de téléphone est requis";
else if (!/^[0-9+]{10,15}$/.test(cleanPhone))
newErrors.telephone =
"Numéro de téléphone invalide (10-15 chiffres)";
if (!formData.username.trim())
newErrors.username = "Le nom d'utilisateur est requis";
else if (formData.username.length < 3)
newErrors.username = "Au moins 3 caractères requis";
if (!formData.password)
newErrors.password = "Le mot de passe est requis";
else if (formData.password.length < 8)
newErrors.password = "Au moins 8 caractères requis";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleRegister = async () => {
if (!validateForm()) return;
setIsLoading(true);
setApiError("");
try {
const result = await registerUser(
formData.username,
formData.password,
formData.nom,
formData.prenom,
formData.telephone,
);
if (result.success && result.access_token) {
await loginClient(result.access_token);
} else {
const message =
result.message || "Erreur lors de l'inscription";
setApiError(message);
setErrors({ username: message });
}
} catch (err: any) {
setApiError(err.message || "Erreur serveur");
} finally {
setIsLoading(false);
}
};
return (
<ScrollView
contentContainerStyle={[
styles.container,
{ backgroundColor: colors.bgSecondary },
]}
>
<View style={[styles.card, { backgroundColor: colors.bgPrimary }]}>
<View style={styles.iconContainer}>
<FontAwesome
name="shopping-bag"
size={48}
color="#7c3aed"
/>
</View>
<Text style={[styles.title, { color: colors.textWhite }]}>
Créer un compte
</Text>
{apiError ? (
<Text style={styles.apiError}>{apiError}</Text>
) : null}
{/* Nom */}
<View style={styles.inputGroup}>
<Feather
name="user"
size={20}
color={colors.textMuted}
style={styles.icon}
/>
<TextInput
style={[
styles.input,
{
color: colors.textWhite,
borderColor: colors.border,
},
errors.nom && styles.inputError,
]}
placeholder="Nom"
placeholderTextColor={colors.textMuted}
value={formData.nom}
onChangeText={(value) => handleChange("nom", value)}
/>
{errors.nom && (
<Text style={styles.errorText}>{errors.nom}</Text>
)}
</View>
{/* Prénom */}
<View style={styles.inputGroup}>
<Feather
name="user"
size={20}
color={colors.textMuted}
style={styles.icon}
/>
<TextInput
style={[
styles.input,
{
color: colors.textWhite,
borderColor: colors.border,
},
errors.prenom && styles.inputError,
]}
placeholder="Prénom"
placeholderTextColor={colors.textMuted}
value={formData.prenom}
onChangeText={(value) => handleChange("prenom", value)}
/>
{errors.prenom && (
<Text style={styles.errorText}>{errors.prenom}</Text>
)}
</View>
{/* Téléphone */}
<View style={styles.inputGroup}>
<Feather
name="phone"
size={20}
color={colors.textMuted}
style={styles.icon}
/>
<TextInput
style={[
styles.input,
{
color: colors.textWhite,
borderColor: colors.border,
},
errors.telephone && styles.inputError,
]}
placeholder="Téléphone"
placeholderTextColor={colors.textMuted}
value={formData.telephone}
keyboardType="phone-pad"
onChangeText={(value) =>
handleChange("telephone", value)
}
/>
{errors.telephone && (
<Text style={styles.errorText}>{errors.telephone}</Text>
)}
</View>
{/* Username */}
<View style={styles.inputGroup}>
<Feather
name="user"
size={20}
color={colors.textMuted}
style={styles.icon}
/>
<TextInput
style={[
styles.input,
{
color: colors.textWhite,
borderColor: colors.border,
},
errors.username && styles.inputError,
]}
placeholder="Nom d'utilisateur"
placeholderTextColor={colors.textMuted}
value={formData.username}
onChangeText={(value) =>
handleChange("username", value)
}
/>
{errors.username && (
<Text style={styles.errorText}>{errors.username}</Text>
)}
</View>
{/* Password */}
<View style={styles.inputGroup}>
<Feather
name="lock"
size={20}
color={colors.textMuted}
style={styles.icon}
/>
<TextInput
style={[
styles.input,
{
color: colors.textWhite,
borderColor: colors.border,
},
errors.password && styles.inputError,
]}
placeholder="Mot de passe"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showPassword}
value={formData.password}
onChangeText={(value) =>
handleChange("password", value)
}
/>
<TouchableOpacity
style={styles.eyeButton}
onPress={() => setShowPassword(!showPassword)}
>
<Feather
name={showPassword ? "eye-off" : "eye"}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
{errors.password && (
<Text style={styles.errorText}>{errors.password}</Text>
)}
</View>
<TouchableOpacity
style={[styles.submitButton, isLoading && { opacity: 0.6 }]}
onPress={handleRegister}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.submitText}>Créer mon compte</Text>
)}
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.navigate("login")}>
<Text style={[styles.loginText, { color: "#a78bfa" }]}>
Vous avez déjà un compte ? Se connecter
</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
export default RegisterScreen;
const styles = StyleSheet.create({
container: {
flexGrow: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
card: { width: "100%", maxWidth: 400, borderRadius: 16, padding: 24 },
iconContainer: { alignItems: "center", marginBottom: 16 },
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 24,
textAlign: "center",
},
inputGroup: { marginBottom: 16, position: "relative" },
icon: { position: "absolute", left: 12, top: 12 },
input: { height: 40, paddingLeft: 40, borderWidth: 1, borderRadius: 8 },
inputError: { borderColor: "#ef4444" },
eyeButton: { position: "absolute", right: 12, top: 8 },
errorText: { color: "#f87171", marginTop: 4 },
apiError: { color: "#f87171", textAlign: "center", marginBottom: 12 },
submitButton: {
backgroundColor: "#7c3aed",
borderRadius: 8,
padding: 12,
alignItems: "center",
marginTop: 8,
},
submitText: { color: "white", fontWeight: "600" },
loginText: { textAlign: "center", marginTop: 16 },
});
@@ -0,0 +1,95 @@
import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { RootStackParamList } from "../../navigation/types";
import { useTheme } from "../../context/ThemeContext";
type Nav = NativeStackNavigationProp<RootStackParamList, "role">;
export default function RoleSelectScreen() {
const navigation = useNavigation<Nav>();
const { colors } = useTheme();
return (
<View
style={[styles.container, { backgroundColor: colors.bgSecondary }]}
>
<View style={[styles.card, { backgroundColor: colors.bgPrimary }]}>
<Ionicons name="people-outline" size={48} color="#7c3aed" />
<Text style={[styles.title, { color: colors.textWhite }]}>
Choisir un role
</Text>
<Text style={[styles.subtitle, { color: colors.textMuted }]}>
Selectionnez votre type de compte
</Text>
<TouchableOpacity
style={[
styles.roleBtn,
{
backgroundColor: colors.bgInput,
borderColor: colors.border,
},
]}
onPress={() => navigation.navigate("login")}
>
<Ionicons name="person-outline" size={24} color="#7c3aed" />
<View style={styles.roleBtnText}>
<Text
style={[
styles.roleTitle,
{ color: colors.textWhite },
]}
>
Client
</Text>
<Text
style={[
styles.roleDesc,
{ color: colors.textMuted },
]}
>
Commander des produits
</Text>
</View>
<Ionicons
name="chevron-forward"
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
card: {
width: "100%",
maxWidth: 400,
borderRadius: 16,
padding: 24,
alignItems: "center",
},
title: { fontSize: 24, fontWeight: "bold", marginTop: 12 },
subtitle: { fontSize: 14, marginTop: 4, marginBottom: 24 },
roleBtn: {
flexDirection: "row",
alignItems: "center",
borderRadius: 12,
padding: 16,
width: "100%",
borderWidth: 1,
},
roleBtnText: { flex: 1, marginLeft: 12 },
roleTitle: { fontSize: 16, fontWeight: "600" },
roleDesc: { fontSize: 12, marginTop: 2 },
});