568 lines
19 KiB
TypeScript
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),
|
|
},
|
|
});
|