chore: finish features comming soon
This commit is contained in:
@@ -7,7 +7,13 @@ import "./ProductCard.css";
|
||||
|
||||
function getTextColor(hex: string): string {
|
||||
const h = hex.replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||
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);
|
||||
@@ -160,7 +166,7 @@ function ProductCard({
|
||||
<div className="sold-out-overlay">SOLD OUT</div>
|
||||
)}
|
||||
{isComingSoon && (
|
||||
<div className="coming-soon-overlay">À VENIR</div>
|
||||
<div className="coming-soon-overlay">COMMING SOON</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -188,7 +194,10 @@ function ProductCard({
|
||||
disabled={isOutOfStock || isComingSoon}
|
||||
style={
|
||||
categoryColor && !isOutOfStock && !isComingSoon
|
||||
? { background: categoryColor, color: getTextColor(categoryColor) }
|
||||
? {
|
||||
background: categoryColor,
|
||||
color: getTextColor(categoryColor),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
@@ -217,8 +226,9 @@ function ProductCard({
|
||||
key={priceOption.quantity}
|
||||
value={priceOption.quantity}
|
||||
>
|
||||
{priceOption.quantity}{unit} -{" "}
|
||||
{priceOption.price.toFixed(2)} €
|
||||
{priceOption.quantity}
|
||||
{unit} - {priceOption.price.toFixed(2)}{" "}
|
||||
€
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -228,7 +238,9 @@ function ProductCard({
|
||||
</div>
|
||||
|
||||
{/* ✨ Modal vidéo — rendu via Portal pour éviter le clipping du transform:scale sur .product-card */}
|
||||
{showVideo && videoUrl && createPortal(
|
||||
{showVideo &&
|
||||
videoUrl &&
|
||||
createPortal(
|
||||
<div className="video-modal" onClick={handleCloseVideo}>
|
||||
<div
|
||||
className="video-modal-content"
|
||||
@@ -252,7 +264,7 @@ function ProductCard({
|
||||
</video>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { getProductById, getCategories, isUserAuthenticated } from "../../api/api";
|
||||
import {
|
||||
getProductById,
|
||||
getCategories,
|
||||
isUserAuthenticated,
|
||||
} from "../../api/api";
|
||||
import type { Product } from "../../api/api";
|
||||
import { useCart } from "../../context/useCart";
|
||||
import Navbar from "../../components/Navbar";
|
||||
@@ -87,9 +91,19 @@ function ProductDetail() {
|
||||
...response.data,
|
||||
prices:
|
||||
response.data.prices
|
||||
?.filter((p: { quantity: number; price: number; active_price?: boolean }) => p.active_price !== false)
|
||||
?.filter(
|
||||
(p: {
|
||||
quantity: number;
|
||||
price: number;
|
||||
active_price?: boolean;
|
||||
}) => p.active_price !== false,
|
||||
)
|
||||
.map(
|
||||
(p: { quantity: number; price: number; active_price?: boolean }) => ({
|
||||
(p: {
|
||||
quantity: number;
|
||||
price: number;
|
||||
active_price?: boolean;
|
||||
}) => ({
|
||||
quantity: parseFloat(String(p.quantity)),
|
||||
price: parseFloat(String(p.price)),
|
||||
active_price: p.active_price,
|
||||
@@ -107,14 +121,20 @@ function ProductDetail() {
|
||||
|
||||
// Couleur de la catégorie depuis la DB
|
||||
const matched = categories.find(
|
||||
(c) => c.name.toLowerCase() === (response.data.category || "").toLowerCase(),
|
||||
(c) =>
|
||||
c.name.toLowerCase() ===
|
||||
(response.data.category || "").toLowerCase(),
|
||||
);
|
||||
if (matched?.color) setCatColor(matched.color);
|
||||
} else {
|
||||
setError(response.message || "Produit non trouvé");
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "Erreur lors du chargement du produit");
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Erreur lors du chargement du produit",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -217,7 +237,8 @@ function ProductDetail() {
|
||||
const r = parseInt(h.slice(0, 2), 16);
|
||||
const g = parseInt(h.slice(2, 4), 16);
|
||||
const b = parseInt(h.slice(4, 6), 16);
|
||||
const catTextColor = (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
|
||||
const catTextColor =
|
||||
(r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -235,21 +256,38 @@ function ProductDetail() {
|
||||
|
||||
<div
|
||||
className="product-detail-container"
|
||||
style={{ "--cat-color": catColor, "--cat-color-rgb": catColorRgb, "--cat-text-color": catTextColor } as React.CSSProperties}
|
||||
style={
|
||||
{
|
||||
"--cat-color": catColor,
|
||||
"--cat-color-rgb": catColorRgb,
|
||||
"--cat-text-color": catTextColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<button onClick={() => navigate(-1)} className="back-button">
|
||||
← Retour
|
||||
</button>
|
||||
|
||||
<div className="product-detail-content">
|
||||
<div className={`product-image-section ${isOutOfStock ? "out-of-stock" : ""}`}>
|
||||
<div
|
||||
className={`product-image-section ${isOutOfStock ? "out-of-stock" : ""}`}
|
||||
>
|
||||
<img
|
||||
src={product.media?.find(m => m.type === "image")?.url || ""}
|
||||
src={
|
||||
product.media?.find((m) => m.type === "image")
|
||||
?.url || ""
|
||||
}
|
||||
alt={product.name}
|
||||
className="product-detail-image"
|
||||
/>
|
||||
{isOutOfStock && <div className="sold-out-badge">SOLD OUT</div>}
|
||||
{isComingSoon && <div className="coming-soon-badge">À VENIR</div>}
|
||||
{isOutOfStock && (
|
||||
<div className="sold-out-badge">SOLD OUT</div>
|
||||
)}
|
||||
{isComingSoon && (
|
||||
<div className="coming-soon-badge">
|
||||
COMMING SOON
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="product-info-section">
|
||||
@@ -258,7 +296,8 @@ function ProductDetail() {
|
||||
{selectedPrice > 0 && (
|
||||
<p className="product-detail-price">
|
||||
{selectedPrice.toFixed(2)} €{" "}
|
||||
{selectedGrams && `pour ${selectedGrams}${product.unit || "g"}`}
|
||||
{selectedGrams &&
|
||||
`pour ${selectedGrams}${product.unit || "g"}`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -296,7 +335,8 @@ function ProductDetail() {
|
||||
key={p.quantity}
|
||||
value={p.quantity}
|
||||
>
|
||||
{p.quantity}{product.unit || "g"} -{" "}
|
||||
{p.quantity}
|
||||
{product.unit || "g"} -{" "}
|
||||
{p.price.toFixed(2)} €
|
||||
</option>
|
||||
))}
|
||||
@@ -308,7 +348,11 @@ function ProductDetail() {
|
||||
<button
|
||||
className={`add-to-cart-button ${isOutOfStock || isComingSoon || selectedGrams === null ? "disabled" : ""}`}
|
||||
onClick={handleAddToCart}
|
||||
disabled={isOutOfStock || isComingSoon || selectedGrams === null}
|
||||
disabled={
|
||||
isOutOfStock ||
|
||||
isComingSoon ||
|
||||
selectedGrams === null
|
||||
}
|
||||
>
|
||||
{isOutOfStock
|
||||
? "Rupture de stock"
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
|
||||
function getTextColor(hex: string): string {
|
||||
const h = hex.replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||
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);
|
||||
@@ -37,20 +43,29 @@ interface ProductCardProps {
|
||||
stock: number;
|
||||
unit?: string;
|
||||
coming_soon?: boolean;
|
||||
prices?: Array<{ quantity: number; price: number; active_price?: boolean }>;
|
||||
prices?: Array<{
|
||||
quantity: number;
|
||||
price: number;
|
||||
active_price?: boolean;
|
||||
}>;
|
||||
media?: Array<{ url: string; type: string }>;
|
||||
};
|
||||
onPress: () => void;
|
||||
categoryColor?: string;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product, onPress, categoryColor }: ProductCardProps) {
|
||||
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 isComingSoon = product.coming_soon === true;
|
||||
const activePrices = product.prices?.filter((p) => p.active_price !== false) ?? [];
|
||||
const activePrices =
|
||||
product.prices?.filter((p) => p.active_price !== false) ?? [];
|
||||
const firstPrice = activePrices[0]?.price ?? null;
|
||||
|
||||
const imageMedia = product.media?.find((m) => m.type === "image");
|
||||
@@ -161,7 +176,7 @@ export default function ProductCard({ product, onPress, categoryColor }: Product
|
||||
)}
|
||||
{isComingSoon && (
|
||||
<View style={styles.comingSoonOverlay}>
|
||||
<Text style={styles.comingSoonText}>À VENIR</Text>
|
||||
<Text style={styles.comingSoonText}>COMMING SOON</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -297,7 +312,8 @@ export default function ProductCard({ product, onPress, categoryColor }: Product
|
||||
{ color: colors.textWhite },
|
||||
]}
|
||||
>
|
||||
{p.quantity}{product.unit || "g"}
|
||||
{p.quantity}
|
||||
{product.unit || "g"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
|
||||
@@ -70,7 +70,9 @@ export default function ProductDetailScreen() {
|
||||
setSelectedPrice(fixedProduct.prices[0].price);
|
||||
}
|
||||
const matched = categories.find(
|
||||
(c) => c.name.toLowerCase() === (p.category || "").toLowerCase(),
|
||||
(c) =>
|
||||
c.name.toLowerCase() ===
|
||||
(p.category || "").toLowerCase(),
|
||||
);
|
||||
if (matched?.color) setCatColor(matched.color);
|
||||
} else {
|
||||
@@ -111,11 +113,19 @@ export default function ProductDetailScreen() {
|
||||
|
||||
const catTextColor = (() => {
|
||||
const h = catColor.replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||
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";
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 > 128
|
||||
? "#000000"
|
||||
: "#ffffff";
|
||||
})();
|
||||
|
||||
const overlayBg = isDark ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)";
|
||||
@@ -546,7 +556,9 @@ export default function ProductDetailScreen() {
|
||||
)}
|
||||
{isComingSoon && (
|
||||
<View style={styles.comingSoonBadge}>
|
||||
<Text style={styles.comingSoonText}>À VENIR</Text>
|
||||
<Text style={styles.comingSoonText}>
|
||||
COMMING SOON
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{videoUri && !isOutOfStock && (
|
||||
@@ -573,7 +585,8 @@ export default function ProductDetailScreen() {
|
||||
<View style={styles.priceIndicator} />
|
||||
<Text style={styles.priceText}>
|
||||
{selectedPrice.toFixed(2)} €{" "}
|
||||
{selectedGrams && `pour ${selectedGrams}${product.unit || "g"}`}
|
||||
{selectedGrams &&
|
||||
`pour ${selectedGrams}${product.unit || "g"}`}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -615,19 +628,26 @@ export default function ProductDetailScreen() {
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addToCartBtn,
|
||||
(isOutOfStock || isComingSoon || selectedGrams === null) &&
|
||||
(isOutOfStock ||
|
||||
isComingSoon ||
|
||||
selectedGrams === null) &&
|
||||
styles.addToCartBtnDisabled,
|
||||
]}
|
||||
onPress={handleAddToCart}
|
||||
disabled={
|
||||
isOutOfStock || isComingSoon || selectedGrams === null || adding
|
||||
isOutOfStock ||
|
||||
isComingSoon ||
|
||||
selectedGrams === null ||
|
||||
adding
|
||||
}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.addToCartText,
|
||||
(isOutOfStock || isComingSoon || selectedGrams === null) &&
|
||||
(isOutOfStock ||
|
||||
isComingSoon ||
|
||||
selectedGrams === null) &&
|
||||
styles.addToCartTextDisabled,
|
||||
]}
|
||||
>
|
||||
@@ -691,7 +711,8 @@ export default function ProductDetailScreen() {
|
||||
},
|
||||
]}
|
||||
>
|
||||
{p.quantity}{product.unit || "g"}
|
||||
{p.quantity}
|
||||
{product.unit || "g"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
|
||||
Reference in New Issue
Block a user