chore: v1.0.0
Omnex Plateform App - EAS Build / build (push) Failing after 8m4s

This commit is contained in:
Xor290
2026-08-04 18:47:47 +02:00
parent 736ec86747
commit 877291e684
69 changed files with 35272 additions and 0 deletions
@@ -0,0 +1,307 @@
import React, { useState, useRef, useMemo } from "react";
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
ScrollView,
TouchableOpacity,
TextInput as RNTextInput,
Animated,
useWindowDimensions,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { useAuth } from "../../auth/AuthContext";
import { loginAdmin } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
export default function AdminLoginScreen() {
const { colors } = useTheme();
const { width: screenWidth } = useWindowDimensions();
const ACCENT = colors.accent;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { loginAdmin: authLogin } = useAuth();
const navigation = useNavigation();
const buttonScale = useRef(new Animated.Value(1)).current;
const { alert, showError, hideAlert } = useAlert();
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
accentBar: { height: 4, width: "100%" },
heroSection: {
alignItems: "center",
paddingTop: screenWidth < 380 ? 36 : 60,
paddingBottom: spacing.l,
},
heroBg: {
width: screenWidth < 380 ? 96 : 120,
height: screenWidth < 380 ? 96 : 120,
borderRadius: screenWidth < 380 ? 48 : 60,
justifyContent: "center",
alignItems: "center",
},
heroInner: {
width: screenWidth < 380 ? 68 : 88,
height: screenWidth < 380 ? 68 : 88,
borderRadius: screenWidth < 380 ? 34 : 44,
justifyContent: "center",
alignItems: "center",
},
heroTitle: {
fontSize: screenWidth < 380 ? fontSize.xl : fontSize.xxl,
fontWeight: "800",
color: colors.textPrimary,
marginTop: spacing.m,
letterSpacing: 0.5,
},
heroSubtitle: {
fontSize: screenWidth < 380 ? fontSize.sm : fontSize.md,
color: colors.textMuted,
marginTop: spacing.xs,
},
formSection: { paddingHorizontal: screenWidth < 380 ? spacing.l : spacing.xl, flex: 1 },
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
overflow: "hidden",
},
inputIconBox: {
width: 52,
height: 52,
justifyContent: "center",
alignItems: "center",
},
input: {
flex: 1,
color: colors.textPrimary,
fontSize: fontSize.md,
paddingVertical: 15,
paddingHorizontal: spacing.m,
},
eyeBtn: { padding: spacing.m },
loginBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: 52,
borderRadius: borderRadius.md,
marginTop: spacing.m,
},
loginBtnText: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
backLink: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: spacing.xl,
paddingVertical: spacing.m,
},
backText: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginLeft: spacing.xs,
},
}),
[colors, screenWidth],
);
const handleLogin = async () => {
if (!username.trim() || !password.trim()) {
showError("Erreur", "Veuillez remplir tous les champs");
return;
}
setLoading(true);
try {
const result = await loginAdmin(username.trim(), password);
if (result.success && result.access_token) {
await authLogin(result.access_token, "admin");
} else {
showError("Erreur", result.message || "Connexion échouée");
}
} catch {
showError("Erreur", "Erreur de connexion");
} finally {
setLoading(false);
}
};
const onPressIn = () =>
Animated.spring(buttonScale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
const onPressOut = () =>
Animated.spring(buttonScale, {
toValue: 1,
useNativeDriver: true,
}).start();
return (
<KeyboardAvoidingView
style={styles.container}
behavior="height"
>
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Decorative top accent */}
<View style={[styles.accentBar, { backgroundColor: ACCENT }]} />
<View style={styles.heroSection}>
<View
style={[styles.heroBg, { backgroundColor: ACCENT + "12" }]}
>
<View
style={[
styles.heroInner,
{ backgroundColor: ACCENT + "20" },
]}
>
<Ionicons
name="shield-checkmark"
size={48}
color={ACCENT}
/>
</View>
</View>
<Text style={styles.heroTitle}>Administration</Text>
<Text style={styles.heroSubtitle}>
Accès au panneau de contrôle
</Text>
</View>
<View style={styles.formSection}>
{/* Username */}
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="person-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Nom d'utilisateur"
placeholderTextColor={colors.textMuted}
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
{/* Password */}
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="lock-closed-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Mot de passe"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeBtn}
>
<Ionicons
name={
showPassword ? "eye-off-outline" : "eye-outline"
}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
{/* Login button */}
<Animated.View style={{ transform: [{ scale: buttonScale }] }}>
<TouchableOpacity
style={[styles.loginBtn, { backgroundColor: ACCENT }]}
onPress={handleLogin}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={loading}
activeOpacity={0.9}
>
{loading ? (
<Text style={styles.loginBtnText}>
Connexion...
</Text>
) : (
<>
<Text style={styles.loginBtnText}>
Se connecter
</Text>
<Ionicons
name="arrow-forward"
size={20}
color={colors.white}
style={{ marginLeft: spacing.s }}
/>
</>
)}
</TouchableOpacity>
</Animated.View>
{/* Back link */}
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backLink}
>
<Ionicons
name="chevron-back"
size={18}
color={colors.textMuted}
/>
<Text style={styles.backText}>Choisir un autre rôle</Text>
</TouchableOpacity>
</View>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</ScrollView>
</KeyboardAvoidingView>
);
}
@@ -0,0 +1,298 @@
import React, { useState, useRef, useMemo } from "react";
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
ScrollView,
TouchableOpacity,
TextInput as RNTextInput,
Animated,
useWindowDimensions,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { useAuth } from "../../auth/AuthContext";
import { loginAdmin } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
export default function CabineLoginScreen() {
const { colors } = useTheme();
const { width: screenWidth } = useWindowDimensions();
const ACCENT = colors.info;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { loginAdmin: authLogin } = useAuth();
const navigation = useNavigation();
const buttonScale = useRef(new Animated.Value(1)).current;
const { alert, showError, hideAlert } = useAlert();
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
accentBar: { height: 4, width: "100%" },
heroSection: {
alignItems: "center",
paddingTop: screenWidth < 380 ? 36 : 60,
paddingBottom: spacing.l,
},
heroBg: {
width: screenWidth < 380 ? 96 : 120,
height: screenWidth < 380 ? 96 : 120,
borderRadius: screenWidth < 380 ? 48 : 60,
justifyContent: "center",
alignItems: "center",
},
heroInner: {
width: screenWidth < 380 ? 68 : 88,
height: screenWidth < 380 ? 68 : 88,
borderRadius: screenWidth < 380 ? 34 : 44,
justifyContent: "center",
alignItems: "center",
},
heroTitle: {
fontSize: screenWidth < 380 ? fontSize.xl : fontSize.xxl,
fontWeight: "800",
color: colors.textPrimary,
marginTop: spacing.m,
letterSpacing: 0.5,
},
heroSubtitle: {
fontSize: screenWidth < 380 ? fontSize.sm : fontSize.md,
color: colors.textMuted,
marginTop: spacing.xs,
},
formSection: { paddingHorizontal: screenWidth < 380 ? spacing.l : spacing.xl, flex: 1 },
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
overflow: "hidden",
},
inputIconBox: {
width: 52,
height: 52,
justifyContent: "center",
alignItems: "center",
},
input: {
flex: 1,
color: colors.textPrimary,
fontSize: fontSize.md,
paddingVertical: 15,
paddingHorizontal: spacing.m,
},
eyeBtn: { padding: spacing.m },
loginBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: 52,
borderRadius: borderRadius.md,
marginTop: spacing.m,
},
loginBtnText: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
backLink: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: spacing.xl,
paddingVertical: spacing.m,
},
backText: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginLeft: spacing.xs,
},
}),
[colors, screenWidth],
);
const handleLogin = async () => {
if (!username.trim() || !password.trim()) {
showError("Erreur", "Veuillez remplir tous les champs");
return;
}
setLoading(true);
try {
const result = await loginAdmin(username.trim(), password);
if (result.success && result.access_token) {
await authLogin(result.access_token, "cabine");
} else {
showError("Erreur", result.message || "Connexion échouée");
}
} catch {
showError("Erreur", "Erreur de connexion");
} finally {
setLoading(false);
}
};
const onPressIn = () =>
Animated.spring(buttonScale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
const onPressOut = () =>
Animated.spring(buttonScale, {
toValue: 1,
useNativeDriver: true,
}).start();
return (
<KeyboardAvoidingView
style={styles.container}
behavior="height"
>
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={[styles.accentBar, { backgroundColor: ACCENT }]} />
<View style={styles.heroSection}>
<View
style={[styles.heroBg, { backgroundColor: ACCENT + "12" }]}
>
<View
style={[
styles.heroInner,
{ backgroundColor: ACCENT + "20" },
]}
>
<Ionicons name="desktop" size={48} color={ACCENT} />
</View>
</View>
<Text style={styles.heroTitle}>Cabine</Text>
<Text style={styles.heroSubtitle}>
Suivi des commandes et livreurs
</Text>
</View>
<View style={styles.formSection}>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="person-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Nom d'utilisateur"
placeholderTextColor={colors.textMuted}
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="lock-closed-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Mot de passe"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeBtn}
>
<Ionicons
name={
showPassword ? "eye-off-outline" : "eye-outline"
}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
<Animated.View style={{ transform: [{ scale: buttonScale }] }}>
<TouchableOpacity
style={[styles.loginBtn, { backgroundColor: ACCENT }]}
onPress={handleLogin}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={loading}
activeOpacity={0.9}
>
{loading ? (
<Text style={styles.loginBtnText}>
Connexion...
</Text>
) : (
<>
<Text style={styles.loginBtnText}>
Se connecter
</Text>
<Ionicons
name="arrow-forward"
size={20}
color={colors.white}
style={{ marginLeft: spacing.s }}
/>
</>
)}
</TouchableOpacity>
</Animated.View>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backLink}
>
<Ionicons
name="chevron-back"
size={18}
color={colors.textMuted}
/>
<Text style={styles.backText}>Choisir un autre rôle</Text>
</TouchableOpacity>
</View>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</ScrollView>
</KeyboardAvoidingView>
);
}
@@ -0,0 +1,298 @@
import React, { useState, useRef, useMemo } from "react";
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
ScrollView,
TouchableOpacity,
TextInput as RNTextInput,
Animated,
useWindowDimensions,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { useAuth } from "../../auth/AuthContext";
import { loginAdmin } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
export default function DeliveryLoginScreen() {
const { colors } = useTheme();
const { width: screenWidth } = useWindowDimensions();
const ACCENT = colors.success;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { loginAdmin: authLogin } = useAuth();
const navigation = useNavigation();
const buttonScale = useRef(new Animated.Value(1)).current;
const { alert, showError, hideAlert } = useAlert();
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
accentBar: { height: 4, width: "100%" },
heroSection: {
alignItems: "center",
paddingTop: screenWidth < 380 ? 36 : 60,
paddingBottom: spacing.l,
},
heroBg: {
width: screenWidth < 380 ? 96 : 120,
height: screenWidth < 380 ? 96 : 120,
borderRadius: screenWidth < 380 ? 48 : 60,
justifyContent: "center",
alignItems: "center",
},
heroInner: {
width: screenWidth < 380 ? 68 : 88,
height: screenWidth < 380 ? 68 : 88,
borderRadius: screenWidth < 380 ? 34 : 44,
justifyContent: "center",
alignItems: "center",
},
heroTitle: {
fontSize: screenWidth < 380 ? fontSize.xl : fontSize.xxl,
fontWeight: "800",
color: colors.textPrimary,
marginTop: spacing.m,
letterSpacing: 0.5,
},
heroSubtitle: {
fontSize: screenWidth < 380 ? fontSize.sm : fontSize.md,
color: colors.textMuted,
marginTop: spacing.xs,
},
formSection: { paddingHorizontal: screenWidth < 380 ? spacing.l : spacing.xl, flex: 1 },
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
overflow: "hidden",
},
inputIconBox: {
width: 52,
height: 52,
justifyContent: "center",
alignItems: "center",
},
input: {
flex: 1,
color: colors.textPrimary,
fontSize: fontSize.md,
paddingVertical: 15,
paddingHorizontal: spacing.m,
},
eyeBtn: { padding: spacing.m },
loginBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: 52,
borderRadius: borderRadius.md,
marginTop: spacing.m,
},
loginBtnText: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
backLink: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: spacing.xl,
paddingVertical: spacing.m,
},
backText: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginLeft: spacing.xs,
},
}),
[colors, screenWidth],
);
const handleLogin = async () => {
if (!username.trim() || !password.trim()) {
showError("Erreur", "Veuillez remplir tous les champs");
return;
}
setLoading(true);
try {
const result = await loginAdmin(username.trim(), password);
if (result.success && result.access_token) {
await authLogin(result.access_token, "livreur");
} else {
showError("Erreur", result.message || "Connexion échouée");
}
} catch {
showError("Erreur", "Erreur de connexion");
} finally {
setLoading(false);
}
};
const onPressIn = () =>
Animated.spring(buttonScale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
const onPressOut = () =>
Animated.spring(buttonScale, {
toValue: 1,
useNativeDriver: true,
}).start();
return (
<KeyboardAvoidingView
style={styles.container}
behavior="height"
>
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={[styles.accentBar, { backgroundColor: ACCENT }]} />
<View style={styles.heroSection}>
<View
style={[styles.heroBg, { backgroundColor: ACCENT + "12" }]}
>
<View
style={[
styles.heroInner,
{ backgroundColor: ACCENT + "20" },
]}
>
<Ionicons name="bicycle" size={48} color={ACCENT} />
</View>
</View>
<Text style={styles.heroTitle}>Livreur</Text>
<Text style={styles.heroSubtitle}>
Gestion de vos livraisons
</Text>
</View>
<View style={styles.formSection}>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="person-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Nom d'utilisateur"
placeholderTextColor={colors.textMuted}
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="lock-closed-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Mot de passe"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeBtn}
>
<Ionicons
name={
showPassword ? "eye-off-outline" : "eye-outline"
}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
<Animated.View style={{ transform: [{ scale: buttonScale }] }}>
<TouchableOpacity
style={[styles.loginBtn, { backgroundColor: ACCENT }]}
onPress={handleLogin}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={loading}
activeOpacity={0.9}
>
{loading ? (
<Text style={styles.loginBtnText}>
Connexion...
</Text>
) : (
<>
<Text style={styles.loginBtnText}>
Se connecter
</Text>
<Ionicons
name="arrow-forward"
size={20}
color={colors.white}
style={{ marginLeft: spacing.s }}
/>
</>
)}
</TouchableOpacity>
</Animated.View>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backLink}
>
<Ionicons
name="chevron-back"
size={18}
color={colors.textMuted}
/>
<Text style={styles.backText}>Choisir un autre rôle</Text>
</TouchableOpacity>
</View>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</ScrollView>
</KeyboardAvoidingView>
);
}
@@ -0,0 +1,143 @@
import React, { useMemo } 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 { AuthStackParamList } from "../../navigation/types";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
type Nav = NativeStackNavigationProp<AuthStackParamList, "RoleSelect">;
export default function RoleSelectScreen() {
const { colors } = useTheme();
const navigation = useNavigation<Nav>();
const roles = [
{
key: "AdminLogin" as const,
label: "Admin",
desc: "Gestion complète du système",
icon: "shield-outline" as const,
color: colors.accent,
},
{
key: "CabineLogin" as const,
label: "Cabine",
desc: "Suivi des commandes et livreurs",
icon: "desktop-outline" as const,
color: colors.info,
},
{
key: "DeliveryLogin" as const,
label: "Livreur",
desc: "Gestion des livraisons",
icon: "bicycle-outline" as const,
color: colors.success,
},
];
const styles = useMemo(
() =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.bgPrimary,
justifyContent: "center",
alignItems: "center",
padding: spacing.l,
},
card: {
width: "100%",
maxWidth: 400,
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.lg,
padding: spacing.xl,
alignItems: "center",
},
title: {
fontSize: fontSize.xxl,
fontWeight: "bold",
color: colors.textWhite,
marginTop: spacing.m,
},
subtitle: {
color: colors.textSecondary,
fontSize: fontSize.md,
marginTop: spacing.xs,
marginBottom: spacing.xl,
},
roleBtn: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgCard,
borderRadius: borderRadius.md,
padding: spacing.l,
width: "100%",
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
},
iconCircle: {
width: 44,
height: 44,
borderRadius: 22,
justifyContent: "center",
alignItems: "center",
},
roleBtnText: { flex: 1, marginLeft: spacing.m },
roleTitle: {
color: colors.textWhite,
fontSize: fontSize.lg,
fontWeight: "600",
},
roleDesc: {
color: colors.textSecondary,
fontSize: fontSize.sm,
marginTop: 2,
},
}),
[colors],
);
return (
<View style={styles.container}>
<View style={styles.card}>
<Ionicons
name="people-outline"
size={48}
color={colors.accent}
/>
<Text style={styles.title}>Panel Administration</Text>
<Text style={styles.subtitle}>Sélectionnez votre rôle</Text>
{roles.map((r) => (
<TouchableOpacity
key={r.key}
style={styles.roleBtn}
onPress={() => navigation.navigate(r.key)}
activeOpacity={0.7}
>
<View
style={[
styles.iconCircle,
{ backgroundColor: r.color + "20" },
]}
>
<Ionicons name={r.icon} size={24} color={r.color} />
</View>
<View style={styles.roleBtnText}>
<Text style={styles.roleTitle}>{r.label}</Text>
<Text style={styles.roleDesc}>{r.desc}</Text>
</View>
<Ionicons
name="chevron-forward"
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
))}
</View>
</View>
);
}
@@ -0,0 +1,270 @@
import React, { useState, useRef, useMemo, useEffect } from "react";
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
ScrollView,
TouchableOpacity,
TextInput as RNTextInput,
Animated,
useWindowDimensions,
} 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 { AuthStackParamList } from "../../navigation/types";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getStoredServerUrl, saveServerUrl } from "../../api/client";
import { normalizeServerUrl } from "../../utils/serverConfig";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
type Nav = NativeStackNavigationProp<AuthStackParamList, "ServerConfig">;
export default function ServerConfigScreen() {
const { colors } = useTheme();
const { width: screenWidth } = useWindowDimensions();
const ACCENT = colors.accent;
const navigation = useNavigation<Nav>();
const [address, setAddress] = useState("");
const [saving, setSaving] = useState(false);
const buttonScale = useRef(new Animated.Value(1)).current;
const { alert, showError, hideAlert } = useAlert();
useEffect(() => {
(async () => {
const stored = await getStoredServerUrl();
if (stored) setAddress(stored);
})();
}, []);
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
accentBar: { height: 4, width: "100%" },
heroSection: {
alignItems: "center",
paddingTop: screenWidth < 380 ? 36 : 60,
paddingBottom: spacing.l,
},
heroBg: {
width: screenWidth < 380 ? 96 : 120,
height: screenWidth < 380 ? 96 : 120,
borderRadius: screenWidth < 380 ? 48 : 60,
justifyContent: "center",
alignItems: "center",
},
heroInner: {
width: screenWidth < 380 ? 68 : 88,
height: screenWidth < 380 ? 68 : 88,
borderRadius: screenWidth < 380 ? 34 : 44,
justifyContent: "center",
alignItems: "center",
},
heroTitle: {
fontSize: screenWidth < 380 ? fontSize.xl : fontSize.xxl,
fontWeight: "800",
color: colors.textPrimary,
marginTop: spacing.m,
letterSpacing: 0.5,
},
heroSubtitle: {
fontSize: screenWidth < 380 ? fontSize.sm : fontSize.md,
color: colors.textMuted,
marginTop: spacing.xs,
textAlign: "center",
paddingHorizontal: spacing.l,
},
formSection: {
paddingHorizontal:
screenWidth < 380 ? spacing.l : spacing.xl,
flex: 1,
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.xs,
overflow: "hidden",
},
inputIconBox: {
width: 52,
height: 52,
justifyContent: "center",
alignItems: "center",
},
input: {
flex: 1,
color: colors.textPrimary,
fontSize: fontSize.md,
paddingVertical: 15,
paddingHorizontal: spacing.m,
},
hint: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginBottom: spacing.m,
marginLeft: spacing.xs,
},
continueBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: 52,
borderRadius: borderRadius.md,
marginTop: spacing.m,
},
continueBtnText: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
}),
[colors, screenWidth, ACCENT],
);
const handleContinue = async () => {
const normalized = normalizeServerUrl(address);
if (!normalized) {
showError(
"Adresse invalide",
"Utilisez une URL (https://exemple.com) ou une adresse IP:Port (192.168.1.10:8000)",
);
return;
}
setSaving(true);
try {
await saveServerUrl(normalized);
navigation.replace("RoleSelect");
} finally {
setSaving(false);
}
};
const onPressIn = () =>
Animated.spring(buttonScale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
const onPressOut = () =>
Animated.spring(buttonScale, {
toValue: 1,
useNativeDriver: true,
}).start();
return (
<KeyboardAvoidingView style={styles.container} behavior="height">
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={[styles.accentBar, { backgroundColor: ACCENT }]} />
<View style={styles.heroSection}>
<View
style={[
styles.heroBg,
{ backgroundColor: ACCENT + "12" },
]}
>
<View
style={[
styles.heroInner,
{ backgroundColor: ACCENT + "20" },
]}
>
<Ionicons
name="server-outline"
size={48}
color={ACCENT}
/>
</View>
</View>
<Text style={styles.heroTitle}>Serveur API</Text>
<Text style={styles.heroSubtitle}>
Renseignez l'adresse du serveur avant de vous
connecter
</Text>
</View>
<View style={styles.formSection}>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="globe-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="192.168.1.10:8000 ou https://exemple.com"
placeholderTextColor={colors.textMuted}
value={address}
onChangeText={setAddress}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
/>
</View>
<Text style={styles.hint}>
URL complète ou adresse IP suivie du port
</Text>
<Animated.View
style={{ transform: [{ scale: buttonScale }] }}
>
<TouchableOpacity
style={[
styles.continueBtn,
{ backgroundColor: ACCENT },
]}
onPress={handleContinue}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={saving}
activeOpacity={0.9}
>
{saving ? (
<Text style={styles.continueBtnText}>
Enregistrement...
</Text>
) : (
<>
<Text style={styles.continueBtnText}>
Continuer
</Text>
<Ionicons
name="arrow-forward"
size={20}
color={colors.white}
style={{ marginLeft: spacing.s }}
/>
</>
)}
</TouchableOpacity>
</Animated.View>
</View>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</ScrollView>
</KeyboardAvoidingView>
);
}