Files
projet_gestion_commande/mobile/src/components/ProductCard.tsx
T
2026-04-19 18:55:58 +02:00

568 lines
19 KiB
TypeScript

import React, { useState } from "react";
import {
View,
Text,
TouchableOpacity,
Image,
StyleSheet,
Dimensions,
Modal,
Pressable,
ScrollView,
} from "react-native";
function getTextColor(hex: string): string {
const h = hex.replace("#", "");
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
const r = parseInt(full.slice(0, 2), 16);
const g = parseInt(full.slice(2, 4), 16);
const b = parseInt(full.slice(4, 6), 16);
return (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
}
import { Ionicons } from "@expo/vector-icons";
import { Video, ResizeMode } from "expo-av";
import { spacing, borderRadius, fontSize, fontWeight } from "../theme";
import { useTheme } from "../context/ThemeContext";
import { API_BASE_URL } from "../api/client";
import { useCart } from "../context/CartContext";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const CARD_WIDTH = SCREEN_WIDTH - 48;
interface ProductCardProps {
product: {
id: number;
name: string;
category: string;
stock: number;
unit?: string;
prices?: Array<{ quantity: number; price: number }>;
media?: Array<{ url: string; type: string }>;
};
onPress: () => void;
categoryColor?: string;
}
export default function ProductCard({ product, onPress, categoryColor }: ProductCardProps) {
const { colors } = useTheme();
const { addToCart } = useCart();
const catColor = categoryColor ?? colors.accent;
const isSoldOut = product.stock <= 0;
const firstPrice = product.prices?.[0]?.price ?? null;
const imageMedia = product.media?.find((m) => m.type === "image");
const videoMedia = product.media?.find((m) => m.type === "video");
const imageUri = imageMedia ? `${API_BASE_URL}${imageMedia.url}` : null;
const videoUri = videoMedia ? `${API_BASE_URL}${videoMedia.url}` : null;
const [showQuantitySelect, setShowQuantitySelect] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [showVideo, setShowVideo] = useState(false);
const handleQuickAdd = () => {
if (!isSoldOut && product.prices && product.prices.length > 0) {
setShowQuantitySelect(!showQuantitySelect);
}
};
const handleSelectQuantity = (priceOption: {
quantity: number;
price: number;
}) => {
addToCart({
product_id: product.id,
name_product: product.name,
category: (product.category || "autre").toLowerCase().trim(),
quantity: priceOption.quantity,
price: priceOption.price,
});
setShowSuccess(true);
setShowQuantitySelect(false);
setTimeout(() => setShowSuccess(false), 1500);
};
return (
<View
style={[
styles.card,
{
backgroundColor: colors.bgSecondary,
borderColor: catColor,
shadowColor: catColor,
},
isSoldOut && styles.soldOut,
]}
>
<View
style={[
styles.imageContainer,
{ backgroundColor: colors.bgInput },
]}
>
{imageUri ? (
<Image
source={{ uri: imageUri }}
style={styles.image}
resizeMode="cover"
/>
) : (
<View
style={[
styles.imagePlaceholder,
{ backgroundColor: catColor + "15" },
]}
>
<Ionicons
name="leaf-outline"
size={60}
color={catColor}
/>
</View>
)}
{videoUri && !isSoldOut && (
<TouchableOpacity
style={[
styles.videoBtn,
{ backgroundColor: catColor + "DD" },
]}
onPress={() => setShowVideo(true)}
activeOpacity={0.7}
>
<Ionicons
name="videocam"
size={18}
color={colors.white}
/>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.detailsBtn}
onPress={onPress}
activeOpacity={0.7}
>
<Ionicons
name="information-circle-outline"
size={16}
color={colors.white}
/>
<Text
style={[styles.detailsBtnText, { color: colors.white }]}
>
Details
</Text>
</TouchableOpacity>
{isSoldOut && (
<View style={styles.soldOutOverlay}>
<Text style={styles.soldOutText}>SOLD OUT</Text>
</View>
)}
</View>
<View
style={[
styles.info,
{
backgroundColor: colors.bgSecondary,
borderTopColor: colors.border,
},
]}
>
<Text
style={[styles.name, { color: colors.textWhite }]}
numberOfLines={1}
>
{product.name}
</Text>
<Text style={[styles.price, { color: colors.success }]}>
{firstPrice !== null
? `${firstPrice.toFixed(2)} €`
: "Prix non disponible"}
</Text>
</View>
<View
style={[
styles.quickAddSection,
{
backgroundColor: colors.bgPrimary,
borderTopColor: colors.border,
},
]}
>
{showSuccess ? (
<View
style={[
styles.successBanner,
{ backgroundColor: catColor },
]}
>
<Text
style={[
styles.successText,
{ color: getTextColor(catColor) },
]}
>
Ajoute !
</Text>
</View>
) : (
<TouchableOpacity
style={[
styles.quickAddBtn,
{ backgroundColor: catColor },
isSoldOut && {
backgroundColor: colors.textMuted,
opacity: 0.6,
},
]}
onPress={handleQuickAdd}
disabled={isSoldOut}
activeOpacity={0.7}
>
<Text
style={[
styles.quickAddBtnText,
{ color: getTextColor(catColor) },
]}
>
{isSoldOut
? "Rupture de stock"
: "Ajouter rapidement"}
</Text>
</TouchableOpacity>
)}
</View>
<Modal
visible={showQuantitySelect}
transparent
animationType="slide"
onRequestClose={() => setShowQuantitySelect(false)}
>
<Pressable
style={styles.pickerOverlay}
onPress={() => setShowQuantitySelect(false)}
>
<View
style={[
styles.pickerSheet,
{ backgroundColor: colors.bgSecondary },
]}
>
<View
style={[
styles.pickerHandle,
{ backgroundColor: colors.textMuted },
]}
/>
<Text
style={[
styles.pickerTitle,
{ color: colors.textWhite },
]}
>
Choisir une quantite
</Text>
<ScrollView
style={styles.pickerScroll}
showsVerticalScrollIndicator={false}
>
{product.prices?.map((p) => (
<TouchableOpacity
key={p.quantity}
style={[
styles.pickerOption,
{
backgroundColor: colors.bgInput,
borderColor: catColor + "44",
},
]}
onPress={() => handleSelectQuantity(p)}
activeOpacity={0.6}
>
<View style={styles.pickerOptionLeft}>
<Text
style={[
styles.pickerOptionQty,
{ color: colors.textWhite },
]}
>
{p.quantity}{product.unit || "g"}
</Text>
<Text
style={[
styles.pickerOptionPrice,
{ color: catColor },
]}
>
{p.price.toFixed(2)}
</Text>
</View>
<Ionicons
name="add-circle"
size={28}
color={catColor}
/>
</TouchableOpacity>
))}
</ScrollView>
<TouchableOpacity
style={[
styles.pickerCloseBtn,
{ backgroundColor: colors.border },
]}
onPress={() => setShowQuantitySelect(false)}
>
<Text
style={[
styles.pickerCloseBtnText,
{ color: colors.textSecondary },
]}
>
Fermer
</Text>
</TouchableOpacity>
</View>
</Pressable>
</Modal>
<Modal
visible={showVideo}
transparent
animationType="fade"
onRequestClose={() => setShowVideo(false)}
>
<Pressable
style={styles.videoModalOverlay}
onPress={() => setShowVideo(false)}
>
<View
style={[
styles.videoModalContent,
{ backgroundColor: colors.black },
]}
>
<TouchableOpacity
style={styles.videoCloseBtn}
onPress={() => setShowVideo(false)}
>
<Ionicons
name="close"
size={22}
color={colors.white}
/>
</TouchableOpacity>
{videoUri && (
<Video
source={{ uri: videoUri }}
style={styles.videoPlayer}
useNativeControls
resizeMode={ResizeMode.CONTAIN}
shouldPlay
/>
)}
</View>
</Pressable>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
card: {
borderRadius: 15,
borderWidth: 2,
overflow: "hidden",
width: CARD_WIDTH,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.6,
shadowRadius: 20,
elevation: 8,
},
soldOut: { opacity: 0.75 },
imageContainer: {
width: "100%",
aspectRatio: 1,
position: "relative",
overflow: "hidden",
},
image: { width: "100%", height: "100%" },
imagePlaceholder: {
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center",
},
videoBtn: {
position: "absolute",
top: 12,
right: 12,
width: 40,
height: 40,
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
borderWidth: 2,
borderColor: "rgba(255,255,255,0.3)",
},
detailsBtn: {
position: "absolute",
bottom: 12,
alignSelf: "center",
flexDirection: "row",
alignItems: "center",
gap: 6,
backgroundColor: "rgba(0,0,0,0.85)",
borderRadius: 25,
paddingHorizontal: 20,
paddingVertical: 8,
borderWidth: 2,
borderColor: "rgba(255,255,255,0.3)",
},
detailsBtnText: {
fontSize: 14,
fontWeight: fontWeight.semibold,
letterSpacing: 0.5,
},
soldOutOverlay: {
position: "absolute",
top: "50%",
left: "50%",
transform: [
{ translateX: -80 },
{ translateY: -25 },
{ rotate: "-15deg" },
],
backgroundColor: "rgba(0,0,0,0.8)",
borderWidth: 4,
borderColor: "rgba(255,0,0,0.95)",
paddingHorizontal: 30,
paddingVertical: 12,
},
soldOutText: {
color: "rgba(255,0,0,0.95)",
fontSize: 28,
fontWeight: "900",
letterSpacing: 3,
textTransform: "uppercase",
textShadowColor: "rgba(0,0,0,0.9)",
textShadowOffset: { width: 2, height: 2 },
textShadowRadius: 6,
},
info: { padding: spacing.m, borderTopWidth: 1 },
name: {
fontSize: fontSize.lg,
fontWeight: fontWeight.semibold,
textAlign: "center",
marginBottom: spacing.xs,
},
price: {
fontSize: fontSize.xl,
fontWeight: fontWeight.bold,
textAlign: "center",
},
quickAddSection: { padding: spacing.m, borderTopWidth: 1 },
quickAddBtn: {
width: "100%",
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
alignItems: "center",
},
quickAddBtnText: {
fontSize: fontSize.md,
fontWeight: "700",
textTransform: "uppercase",
letterSpacing: 0.5,
},
successBanner: {
width: "100%",
paddingVertical: 12,
borderRadius: 8,
alignItems: "center",
},
successText: { fontSize: fontSize.md, fontWeight: "700" },
pickerOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.7)",
justifyContent: "flex-end",
},
pickerSheet: {
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 24,
paddingBottom: 40,
maxHeight: "70%",
},
pickerHandle: {
width: 40,
height: 4,
borderRadius: 2,
alignSelf: "center",
marginTop: 12,
marginBottom: 16,
},
pickerTitle: {
fontSize: fontSize.lg,
fontWeight: fontWeight.bold,
textAlign: "center",
marginBottom: 16,
},
pickerScroll: { maxHeight: 350 },
pickerOption: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
borderRadius: 12,
borderWidth: 1,
paddingVertical: 16,
paddingHorizontal: 20,
marginBottom: 10,
},
pickerOptionLeft: { gap: 2 },
pickerOptionQty: { fontSize: fontSize.lg, fontWeight: fontWeight.bold },
pickerOptionPrice: {
fontSize: fontSize.md,
fontWeight: fontWeight.semibold,
},
pickerCloseBtn: {
marginTop: 12,
borderRadius: 10,
paddingVertical: 14,
alignItems: "center",
},
pickerCloseBtnText: {
fontSize: fontSize.md,
fontWeight: fontWeight.semibold,
},
videoModalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.92)",
justifyContent: "center",
alignItems: "center",
},
videoModalContent: {
width: SCREEN_WIDTH - 10,
backgroundColor: "#000",
borderRadius: 12,
overflow: "hidden",
position: "relative",
},
videoCloseBtn: {
position: "absolute",
top: 10,
right: 10,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "rgba(239,68,68,0.85)",
justifyContent: "center",
alignItems: "center",
zIndex: 10,
},
videoPlayer: {
width: "100%",
height: Math.round((SCREEN_WIDTH - 10) * 9 / 16),
},
});