chore: finish features comming soon

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