chore: fix UI bug

This commit is contained in:
2026-03-31 11:30:48 +02:00
parent 25cd6429f2
commit 37103ff0ca
4 changed files with 358 additions and 7 deletions
+163 -2
View File
@@ -3,6 +3,7 @@ import {
View,
Text,
FlatList,
ScrollView,
Image,
TouchableOpacity,
StyleSheet,
@@ -58,6 +59,7 @@ export default function CartScreen() {
const [loadingMedia, setLoadingMedia] = useState(false);
const [removeId, setRemoveId] = useState<number | null>(null);
const [showClearModal, setShowClearModal] = useState(false);
const [showRecapModal, setShowRecapModal] = useState(false);
useFocusEffect(
useCallback(() => {
@@ -249,6 +251,94 @@ export default function CartScreen() {
borderTopWidth: 1,
borderTopColor: colors.border,
},
recapScroll: {
maxHeight: 280,
},
recapItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
recapItemImage: {
width: 44,
height: 44,
borderRadius: borderRadius.sm,
},
recapItemImagePlaceholder: {
width: 44,
height: 44,
borderRadius: borderRadius.sm,
backgroundColor: colors.bgInput,
justifyContent: "center",
alignItems: "center",
},
recapItemInfo: {
flex: 1,
marginLeft: spacing.m,
},
recapItemName: {
color: colors.textWhite,
fontSize: fontSize.sm,
fontWeight: fontWeight.semibold,
},
recapItemQty: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginTop: 2,
},
recapItemPrice: {
color: colors.success,
fontSize: fontSize.sm,
fontWeight: fontWeight.bold,
},
recapDivider: {
height: 1,
backgroundColor: colors.border,
marginVertical: spacing.l,
},
recapTotalRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.xs,
},
recapTotalLabel: {
color: colors.textSecondary,
fontSize: fontSize.md,
fontWeight: fontWeight.medium,
},
recapTotalValue: {
color: colors.success,
fontSize: fontSize.xxl,
fontWeight: fontWeight.bold,
},
recapArticlesCount: {
color: colors.textMuted,
fontSize: fontSize.xs,
},
recapNote: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
backgroundColor: colors.accent + "18",
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.accent + "44",
padding: spacing.m,
marginBottom: spacing.l,
},
recapNoteText: {
color: colors.accentLight,
fontSize: fontSize.xs,
flex: 1,
lineHeight: 16,
},
recapActions: {
flexDirection: "row",
gap: spacing.m,
},
totalRow: {
flexDirection: "row",
justifyContent: "space-between",
@@ -375,8 +465,8 @@ export default function CartScreen() {
</View>
<Button
title="Commander"
onPress={() => navigation.navigate("Checkout")}
variant="success"
onPress={() => setShowRecapModal(true)}
variant="primary"
size="lg"
fullWidth
/>
@@ -423,6 +513,77 @@ export default function CartScreen() {
</View>
</Modal>
<Modal
visible={showRecapModal}
onClose={() => setShowRecapModal(false)}
title="Récapitulatif"
icon="receipt-outline"
iconColor={colors.accent}
>
<ScrollView style={styles.recapScroll} showsVerticalScrollIndicator={false}>
{(enrichedItems.length > 0 ? enrichedItems : cartItems).map((item, idx) => (
<View key={item.id} style={[styles.recapItem, idx === 0 && { borderTopWidth: 1, borderTopColor: colors.borderLight }]}>
{(item as EnrichedItem).imageUri ? (
<Image
source={{ uri: (item as EnrichedItem).imageUri }}
style={styles.recapItemImage}
/>
) : (
<View style={styles.recapItemImagePlaceholder}>
<Ionicons name="leaf-outline" size={20} color={colors.textMuted} />
</View>
)}
<View style={styles.recapItemInfo}>
<Text style={styles.recapItemName} numberOfLines={1}>
{item.name_product}
</Text>
<Text style={styles.recapItemQty}>{item.quantity}g</Text>
</View>
<Text style={styles.recapItemPrice}>{item.price.toFixed(2)} </Text>
</View>
))}
</ScrollView>
<View style={styles.recapDivider} />
<View style={styles.recapTotalRow}>
<Text style={styles.recapTotalLabel}>Total</Text>
<Text style={styles.recapTotalValue}>{cartTotal.toFixed(2)} </Text>
</View>
<Text style={styles.recapArticlesCount}>
{cartCount} article{cartCount > 1 ? "s" : ""}
</Text>
<View style={[styles.recapDivider, { marginTop: spacing.l }]} />
<View style={styles.recapNote}>
<Ionicons name="information-circle-outline" size={16} color={colors.accentLight} />
<Text style={styles.recapNoteText}>
Vous renseignerez votre adresse et vos coordonnées à l'étape suivante.
</Text>
</View>
<View style={styles.recapActions}>
<Button
title="Modifier"
onPress={() => setShowRecapModal(false)}
variant="outline"
size="md"
style={{ flex: 1 }}
/>
<Button
title="Confirmer"
onPress={() => {
setShowRecapModal(false);
navigation.navigate("Checkout");
}}
variant="primary"
size="md"
style={{ flex: 1 }}
/>
</View>
</Modal>
<Modal
visible={showClearModal}
onClose={() => setShowClearModal(false)}
@@ -209,6 +209,23 @@ export default function OrderHistoryScreen() {
fontSize: fontSize.lg,
fontWeight: fontWeight.bold,
},
deliveredBadge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: spacing.m,
paddingVertical: spacing.xs,
borderRadius: borderRadius.xl,
borderWidth: 1.5,
borderColor: "#7c3aed",
},
deliveredBadgeText: {
color: "#a78bfa",
fontSize: fontSize.xs,
fontWeight: fontWeight.bold,
textTransform: "uppercase",
letterSpacing: 0.5,
marginLeft: 4,
},
}),
[colors],
);
@@ -372,7 +389,14 @@ export default function OrderHistoryScreen() {
<Text style={styles.orderIdText}>
Commande #{order.client_order_number}
</Text>
<StatusBadge status={order.status} />
{order.status === "cancelled" ? (
<StatusBadge status={order.status} />
) : (
<View style={styles.deliveredBadge}>
<Ionicons name="checkmark-circle" size={12} color="#a78bfa" />
<Text style={styles.deliveredBadgeText}>Livrée</Text>
</View>
)}
</View>
<View style={styles.orderRow}>
<Ionicons
+4 -3
View File
@@ -58,7 +58,7 @@ export default function ProductsScreen() {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(scaleAnim, {
toValue: 1.35,
toValue: 1.12,
duration: 2000,
useNativeDriver: true,
}),
@@ -178,14 +178,15 @@ export default function ProductsScreen() {
gap: spacing.m,
},
comingSoonText: {
fontSize: 36,
fontSize: Math.min(32, SCREEN_WIDTH * 0.07),
color: "#8E8FE8",
letterSpacing: 4,
letterSpacing: 2,
textAlign: "center",
textTransform: "uppercase",
textShadowColor: "rgba(142, 143, 232, 0.85)",
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 18,
maxWidth: SCREEN_WIDTH * 0.82,
},
comingSoonDesc: {
fontSize: fontSize.md,
+166 -1
View File
@@ -2,19 +2,22 @@ import React, { useState, useCallback, useMemo } from "react";
import {
View,
Text,
TextInput as RNTextInput,
ScrollView,
StyleSheet,
TouchableOpacity,
Alert,
Modal,
KeyboardAvoidingView,
ActivityIndicator,
Platform,
Linking,
} from "react-native";
import { Feather } from "@expo/vector-icons";
import { useFocusEffect } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Ionicons } from "@expo/vector-icons";
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram } from "../../api/api";
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword } from "../../api/api";
import TextInput from "../../components/ui/TextInput";
import Button from "../../components/ui/Button";
import { useTheme } from "../../context/ThemeContext";
@@ -49,6 +52,16 @@ export default function ProfileScreen() {
// Modal confirmation infos par défaut
const [showSaveModal, setShowSaveModal] = useState(false);
// Modal changement de mot de passe
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [currentPwd, setCurrentPwd] = useState("");
const [newPwd, setNewPwd] = useState("");
const [confirmPwd, setConfirmPwd] = useState("");
const [showCurrentPwd, setShowCurrentPwd] = useState(false);
const [showNewPwd, setShowNewPwd] = useState(false);
const [showConfirmPwd, setShowConfirmPwd] = useState(false);
const [savingPassword, setSavingPassword] = useState(false);
const loadData = useCallback(async () => {
setLoadingProfile(true);
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus] = await Promise.all([
@@ -129,6 +142,34 @@ export default function ProfileScreen() {
);
};
const handleChangePassword = async () => {
if (!currentPwd || !newPwd || !confirmPwd) {
Alert.alert("Erreur", "Veuillez remplir tous les champs");
return;
}
if (newPwd.length < 8) {
Alert.alert("Erreur", "Le nouveau mot de passe doit contenir au moins 8 caractères");
return;
}
if (newPwd !== confirmPwd) {
Alert.alert("Erreur", "Les nouveaux mots de passe ne correspondent pas");
return;
}
setSavingPassword(true);
try {
const result = await changePassword(currentPwd, newPwd);
if (result.success) {
setShowPasswordModal(false);
setCurrentPwd(""); setNewPwd(""); setConfirmPwd("");
Alert.alert("Succès", "Mot de passe modifié avec succès");
} else {
Alert.alert("Erreur", result.message || "Erreur inattendue");
}
} finally {
setSavingPassword(false);
}
};
const saveContact = async () => {
setSavingContact(true);
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
@@ -274,6 +315,18 @@ export default function ProfileScreen() {
},
modalBtnCancelText: { color: colors.textMuted, fontSize: fontSize.sm, fontWeight: fontWeight.semibold },
modalBtnConfirmText: { color: colors.accent, fontSize: fontSize.sm, fontWeight: fontWeight.semibold },
pwdInputWrapper: {
flexDirection: "row" as const,
alignItems: "center" as const,
borderWidth: 1,
borderRadius: borderRadius.sm,
paddingHorizontal: spacing.m,
height: 46,
marginBottom: spacing.m,
width: "100%",
},
pwdInputIcon: { marginRight: spacing.s },
pwdInput: { flex: 1, fontSize: fontSize.sm },
}), [colors]);
if (loadingProfile) {
@@ -398,6 +451,21 @@ export default function ProfileScreen() {
</TouchableOpacity>
</View>
{/* Carte Changer le mot de passe */}
<View style={styles.card}>
<View style={styles.cardTitle}>
<Ionicons name="lock-closed-outline" size={18} color="#f59e0b" />
<Text style={styles.cardTitleText}>Sécurité</Text>
</View>
<TouchableOpacity
style={[styles.saveBtn, { backgroundColor: "#f59e0b22", borderWidth: 1, borderColor: "#f59e0b66" }]}
onPress={() => setShowPasswordModal(true)}
>
<Ionicons name="lock-closed-outline" size={16} color="#f59e0b" />
<Text style={[styles.saveBtnText, { color: "#f59e0b" }]}>Changer le mot de passe</Text>
</TouchableOpacity>
</View>
{/* Carte Telegram */}
{telegramEnabled && (
<View style={styles.card}>
@@ -478,6 +546,103 @@ export default function ProfileScreen() {
</TouchableOpacity>
</Modal>
{/* Modal changement de mot de passe */}
<Modal
visible={showPasswordModal}
transparent
animationType="slide"
onRequestClose={() => setShowPasswordModal(false)}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={() => setShowPasswordModal(false)}
>
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
<View style={[styles.modalBox, { paddingBottom: spacing.xl }]}>
<View style={styles.modalIconWrap}>
<Ionicons name="lock-closed-outline" size={24} color="#f59e0b" />
</View>
<Text style={styles.modalTitle}>Changer le mot de passe</Text>
{/* Champ mot de passe actuel */}
<View style={[styles.pwdInputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="lock" size={16} color={colors.textMuted} style={styles.pwdInputIcon} />
<RNTextInput
style={[styles.pwdInput, { color: colors.textPrimary }]}
placeholder="Mot de passe actuel"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showCurrentPwd}
value={currentPwd}
onChangeText={setCurrentPwd}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowCurrentPwd(!showCurrentPwd)}>
<Feather name={showCurrentPwd ? "eye-off" : "eye"} size={16} color={colors.textMuted} />
</TouchableOpacity>
</View>
{/* Nouveau mot de passe */}
<View style={[styles.pwdInputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="key" size={16} color={colors.textMuted} style={styles.pwdInputIcon} />
<RNTextInput
style={[styles.pwdInput, { color: colors.textPrimary }]}
placeholder="Nouveau mot de passe (min. 8 car.)"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showNewPwd}
value={newPwd}
onChangeText={setNewPwd}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowNewPwd(!showNewPwd)}>
<Feather name={showNewPwd ? "eye-off" : "eye"} size={16} color={colors.textMuted} />
</TouchableOpacity>
</View>
{/* Confirmer nouveau mot de passe */}
<View style={[styles.pwdInputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="check-circle" size={16} color={colors.textMuted} style={styles.pwdInputIcon} />
<RNTextInput
style={[styles.pwdInput, { color: colors.textPrimary }]}
placeholder="Confirmer le nouveau mot de passe"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showConfirmPwd}
value={confirmPwd}
onChangeText={setConfirmPwd}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowConfirmPwd(!showConfirmPwd)}>
<Feather name={showConfirmPwd ? "eye-off" : "eye"} size={16} color={colors.textMuted} />
</TouchableOpacity>
</View>
<View style={styles.modalActions}>
<TouchableOpacity
style={styles.modalBtnCancel}
onPress={() => setShowPasswordModal(false)}
disabled={savingPassword}
>
<Text style={styles.modalBtnCancelText}>Annuler</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalBtnConfirm, { borderColor: "#f59e0b66" }]}
onPress={handleChangePassword}
disabled={savingPassword}
>
{savingPassword
? <ActivityIndicator size="small" color="#f59e0b" />
: <Text style={[styles.modalBtnConfirmText, { color: "#f59e0b" }]}>Confirmer</Text>
}
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</KeyboardAvoidingView>
);
}