/**
* @file Home.js
* @description Écran d'accueil de l'application.
* Affiche deux sections de produits récupérées en parallèle depuis l'API :
* les articles en vedette et les articles en promotion, sous forme de listes horizontales défilantes.
*/
import React, {Component, useEffect, useState} from 'react';
import {EXPO_PUBLIC_API_URL} from "../config";
import {
ActivityIndicator,
FlatList,
Image,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Dimensions
} from 'react-native';
import {useNavigation} from "@react-navigation/native";
import {GlobalStyles} from "../styles/GlobalStyles";
import ProtectedRoute from "../components/ProtectedRoute";
/** Largeur de l'écran du device, utilisée pour le calcul de la largeur des cartes. */
const { width: screenWidth } = Dimensions.get('window');
/**
* Écran d'accueil affichant les produits en vedette et en promotion.
*
* @component
* @returns {React.JSX.Element} L'interface de la page d'accueil avec les listes de produits.
*/
export default function Home() {
/** @type {[Array<Object>, Function]} Liste des produits en vedette. */
const [featuredData, setFeaturedData] = useState([]);
/** @type {[Array<Object>, Function]} Liste des produits en promotion. */
const [promoData, setPromoData] = useState([]);
/** @type {[boolean, Function]} Indicateur de chargement des données. */
const [loading, setLoading] = useState(true);
/** @type {[string|null, Function]} Message d'erreur en cas d'échec de fetch. */
const [error, setError] = useState(null);
const navigation = useNavigation();
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
/**
* Récupère en parallèle les produits en vedette et en promotion depuis l'API.
* Utilise Promise.all pour optimiser les temps de chargement.
* En cas d'erreur réseau ou API, met à jour l'état d'erreur.
*
* @async
* @returns {Promise<void>}
*/
const fetchAllData = async () => {
setLoading(true);
setError(null);
try {
// 1. On prépare les deux requêtes
const featuredPromise = fetch(
`${EXPO_PUBLIC_API_URL}/produits/vedette`,
{ signal }
);
const promoPromise = fetch(
`${EXPO_PUBLIC_API_URL}/produits/promotion`,
{ signal }
);
// 2. On attend les DEUX
const [featuredResponse, promoResponse] = await Promise.all([
featuredPromise,
promoPromise
]);
// 3. On vérifie les DEUX
if (!featuredResponse.ok) {
const errorData = await featuredResponse.json().catch(() => ({}));
throw new Error(errorData.message || "Erreur lors de la récupération des produits vedette.");
}
if (!promoResponse.ok) {
const errorData = await promoResponse.json().catch(() => ({}));
throw new Error(errorData.message || "Erreur lors de la récupération des promotions.");
}
// 4. On récupère le JSON des DEUX
const [featuredList, promoList] = await Promise.all([
featuredResponse.json(),
promoResponse.json()
]);
setFeaturedData(featuredList);
setPromoData(promoList);
} catch(error) {
if (error.name !== 'AbortError') {
console.error("Erreur de fetch produits:", error.message);
setError(error.message || "Impossible de charger les produits. Veuillez réessayer.");
}
} finally {
setLoading(false);
}
};
fetchAllData();
return () => abortController.abort();
}, []);
if (loading) {
return (
<View style={GlobalStyles.loadingContainer}>
<ActivityIndicator size="large" color="#1e3c72" />
<Text style={GlobalStyles.loadingText}>Chargement des produits...</Text>
</View>
);
}
if (error) {
return (
<View style={GlobalStyles.container}>
<Text style={GlobalStyles.errorText}>{error}</Text>
</View>
);
}
if (featuredData.length === 0 && promoData.length === 0) {
return (
<View style={GlobalStyles.container}>
<Text style={GlobalStyles.emptyText}>Aucun produit disponible pour le moment.</Text>
</View>
);
}
/**
* Navigue vers l'écran de détail d'un produit.
*
* @param {Object} item - Le produit sélectionné.
*/
const handleConsult = (item) => {
navigation.navigate('ProductDetail', { product: item });
};
/**
* Déclenche l'ajout d'un produit au panier.
* (Logique à implémenter via le store.)
*
* @param {Object} item - Le produit à ajouter au panier.
*/
const handleAddToCart = (item) => {
console.log("Ajout au panier:", item.designation);
// Logique d'ajout au panier ici
};
/**
* Largeur d'une carte produit, calculée à 85% de la largeur de l'écran.
* @type {number}
*/
const cardWidth = screenWidth * 0.85;
/**
* Intervalle de snap pour la FlatList horizontale, égal à la largeur d'une carte + sa marge.
* @type {number}
*/
const snapInterval = cardWidth + (styles.cardWrapper.marginRight || 0);
return (
<ScrollView style={GlobalStyles.profileScreen}>
{/* Articles en Vedette — section affichée uniquement s'il y a des données */}
{featuredData.length > 0 && (
<View>
<Text style={styles.sectionTitle}>Les articles en Vedette ✨</Text>
<FlatList
data={featuredData}
keyExtractor={(item) => 'feat-' + item.reference.toString()}
horizontal={true}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalFlatListContainer}
snapToInterval={snapInterval}
decelerationRate="fast"
renderItem={({ item }) => (
<View style={[styles.cardWrapper, { width: cardWidth }]}>
<TouchableOpacity onPress={() => navigation.navigate("ProductsCard", item)}>
<View style={styles.productCardHorizontal}>
<Image
source={{ uri:`${EXPO_PUBLIC_API_URL}` + "/images/produits/" + item.imageUrl }}
style={styles.productImageHorizontal}
/>
</View>
</TouchableOpacity>
<Text style={styles.productNameHorizontal}>{item.designation}</Text>
<Text style={styles.productPriceHorizontal}>{item.prix_unitaire_HT} €</Text>
</View>
)}
/>
</View>
)}
{/* Articles en Promotion — section affichée uniquement s'il y a des données */}
{promoData.length > 0 && (
<View>
<Text style={styles.sectionTitle}>Les articles en PROMOTION ✨</Text>
<FlatList
data={promoData}
keyExtractor={(item) => 'promo-' + item.reference.toString()}
horizontal={true}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalFlatListContainer}
snapToInterval={snapInterval}
decelerationRate="fast"
renderItem={({ item }) => (
<View style={[styles.cardWrapper, { width: cardWidth }]}>
<TouchableOpacity onPress={() => navigation.navigate("ProductsCard", item)}>
<View style={styles.productCardHorizontal}>
<Image
source={{ uri:`${EXPO_PUBLIC_API_URL}`+"/images/produits/" + item.imageUrl }}
style={styles.productImageHorizontal}
/>
</View>
</TouchableOpacity>
<Text style={styles.productNameHorizontal}>{item.designation}</Text>
{/* Prix barré (ancien prix) et nouveau prix promotion */}
<View style={styles.promoPriceContainer}>
<Text style={styles.oldPrice}>{item.prix_initiale} €</Text>
<Text style={styles.newPrice}>{item.nouveau_prix} €</Text>
</View>
</View>
)}
/>
</View>
)}
</ScrollView>
);
};
/**
* Styles locaux de l'écran Home.
* @type {import('react-native').StyleSheet.NamedStyles<any>}
*/
const styles = StyleSheet.create({
cardScreen: {
flex: 1,
backgroundColor: '#f0f2f5',
},
cardContainer: {
backgroundColor: '#ffffff',
margin: 15,
borderRadius: 12,
padding: 20,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 5,
},
productDetailImage: {
width: '100%',
height: 250,
resizeMode: 'contain',
borderRadius: 10,
marginBottom: 20,
},
productDetailName: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
textAlign: 'center',
},
productDetailPrice: {
fontSize: 28,
fontWeight: 'bold',
color: '#1e3c72',
marginBottom: 20,
textAlign: 'center',
},
productDetailDescription: {
fontSize: 16,
color: '#555',
textAlign: 'left',
marginBottom: 20,
lineHeight: 24,
},
addToCartButton: {
backgroundColor: '#28a745',
paddingVertical: 15,
paddingHorizontal: 30,
borderRadius: 8,
marginTop: 20,
},
addToCartButtonText: {
color: '#ffffff',
fontSize: 18,
fontWeight: 'bold',
},
descriptionContainer: {
width: '100%',
marginBottom: 20,
},
descriptionText: {
fontSize: 16,
color: '#555',
textAlign: 'left',
lineHeight: 24,
},
descriptionTitle: {
fontWeight: 'bold',
color: '#333',
fontSize: 17,
},
descriptionBold: {
fontWeight: 'bold',
color: '#555',
},
// --- STYLES POUR LA FLATLIST HORIZONTALE ---
sectionTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#333',
marginLeft: 15,
marginTop: 20,
marginBottom: 15,
},
horizontalFlatListContainer: {
paddingHorizontal: 15,
paddingBottom: 20,
},
cardWrapper: {
height: 280,
marginRight: 10,
},
productCardHorizontal: {
backgroundColor: '#ffffff',
borderRadius: 12,
height: 220,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 5,
position: 'relative',
overflow: 'hidden',
},
productImageHorizontal: {
width: '80%',
height: '80%',
resizeMode: 'contain',
},
productNameHorizontal: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
marginTop: 10,
textAlign: 'left',
paddingHorizontal: 5,
},
productPriceHorizontal: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e3c72',
marginTop: 5,
textAlign: 'left',
paddingHorizontal: 5,
},
promoTag: {
position: 'absolute',
top: 15,
left: 15,
backgroundColor: 'white',
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 6,
zIndex: 1,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 3,
},
promoTagText: {
color: 'black',
fontSize: 12,
fontWeight: 'bold',
marginLeft: 4,
},
addToCartIcon: {
position: 'absolute',
bottom: 15,
right: 15,
backgroundColor: '#007bff',
borderRadius: 20,
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
},
addToCartIconText: {
color: 'white',
fontSize: 24,
lineHeight: 24,
fontWeight: 'bold',
},
promoCard: {
flex: 1,
margin: 5,
backgroundColor: 'white',
borderRadius: 8,
padding: 10,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
maxWidth: '48%',
},
// Styles pour les boutons
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
marginTop: 8,
marginBottom: 8,
},
consultButton: {
backgroundColor: '#6c757d',
paddingVertical: 6,
paddingHorizontal: 8,
borderRadius: 5,
flex: 1,
marginRight: 4,
alignItems: 'center',
},
addButton: {
backgroundColor: '#28a745',
paddingVertical: 6,
paddingHorizontal: 8,
borderRadius: 5,
flex: 1,
marginLeft: 4,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontSize: 12,
fontWeight: 'bold',
},
// Styles pour les prix barrés
promoPriceContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 5,
paddingHorizontal: 5,
},
oldPrice: {
fontSize: 14,
color: '#6c757d',
textDecorationLine: 'line-through',
marginRight: 8,
},
newPrice: {
fontSize: 18,
fontWeight: 'bold',
color: '#D90429',
},
});