Vendaweb-portal/components/ProductCard.tsx

288 lines
10 KiB
TypeScript
Raw Normal View History

2026-01-08 12:09:16 +00:00
import React, { useState } from "react";
import { Product, OrderItem } from "../types";
import NoImagePlaceholder from "./NoImagePlaceholder";
import ImageZoomModal from "./ImageZoomModal";
import ProductStoreDeliveryModal from "./ProductStoreDeliveryModal";
import { authService } from "../src/services/auth.service";
interface ProductCardProps {
product: Product;
onAddToCart: (p: Product | OrderItem) => void;
onViewDetails: (product: Product, quantity?: number) => void;
}
/**
* ProductCard Component
* Componente reutilizável para exibir um card de produto na lista de produtos
*
* @param product - Dados do produto a ser exibido
* @param onAddToCart - Callback para adicionar produto ao carrinho
* @param onViewDetails - Callback para abrir modal de detalhes
*/
const ProductCard: React.FC<ProductCardProps> = ({
product: p,
onAddToCart,
onViewDetails,
}) => {
const [quantity, setQuantity] = useState(1);
const [imageError, setImageError] = useState(false);
const [showImageZoom, setShowImageZoom] = useState(false);
const [showStoreDeliveryModal, setShowStoreDeliveryModal] = useState(false);
// Calcular parcelamento (10x)
const installmentValue = p.price / 10;
return (
<div className="bg-white rounded-[2rem] shadow-sm border border-slate-100 overflow-hidden flex flex-col h-full group hover:shadow-2xl hover:shadow-slate-200 transition-all">
{/* Imagem do Produto */}
<div className="h-64 bg-slate-50 relative overflow-hidden flex items-center justify-center p-5 group/image">
{p.image &&
!p.image.includes("placeholder") &&
p.image !==
"https://placehold.co/200x200/f8fafc/949494/png?text=Sem+Imagem" &&
!imageError ? (
<>
<img
src={p.image}
alt={p.name}
className="max-h-full object-contain mix-blend-multiply group-hover:scale-110 transition-transform duration-500"
onError={() => {
setImageError(true);
}}
/>
{/* Botão de Zoom */}
<button
onClick={() => setShowImageZoom(true)}
className="absolute bottom-4 right-4 bg-[#002147]/80 hover:bg-[#002147] text-white p-2.5 rounded-lg transition-all opacity-0 group-hover/image:opacity-100 shadow-lg backdrop-blur-sm"
title="Ampliar imagem"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"
/>
</svg>
</button>
</>
) : (
<NoImagePlaceholder size="md" />
)}
{/* Badge de Desconto */}
{typeof p.discount === "number" && p.discount > 0 && (
<div className="absolute top-4 left-4 flex items-center gap-2">
<span className="bg-orange-500 text-white px-3 py-1.5 rounded-lg text-xs font-black uppercase tracking-wider shadow-lg">
{p.discount.toFixed(2)}%
</span>
<div className="bg-orange-500 text-white p-2 rounded-full shadow-lg">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
)}
{/* Botão Ver Mais */}
<button
onClick={() => onViewDetails(p)}
className="absolute top-4 right-4 bg-[#002147] text-white px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wide hover:bg-[#003366] transition-all shadow-lg"
>
Ver mais
</button>
</div>
{/* Informações do Produto */}
<div className="p-6 flex-1 flex flex-col">
{/* Título do Produto */}
<h4 className="text-lg font-extrabold text-[#002147] leading-tight mb-2 line-clamp-2">
{p.name}
</h4>
{/* Descrição Detalhada (se disponível) */}
{p.description && (
<p className="text-sm text-slate-600 mb-3 line-clamp-2">
{p.description}
</p>
)}
{/* Informações: Marca - Código - EAN - Modelo */}
{(() => {
const parts: string[] = [];
if (p.mark && p.mark !== "0" && p.mark !== "Sem marca") {
parts.push(p.mark);
}
if (p.code && p.code !== "0") {
parts.push(p.code);
}
if (p.ean && p.ean !== "0") {
parts.push(p.ean);
}
if (p.model && p.model !== "0") {
parts.push(p.model);
}
return parts.length > 0 ? (
<div className="mb-4">
<p className="text-xs text-slate-500 font-medium leading-relaxed">
{parts.join(" - ")}
</p>
</div>
) : null;
})()}
{/* Preços */}
<div className="mb-4">
{p.originalPrice && p.originalPrice > p.price && (
<p className="text-sm text-slate-400 line-through mb-1">
de R${" "}
{p.originalPrice.toLocaleString("pt-BR", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
)}
<div className="flex items-center gap-3">
<p className="text-3xl font-black text-orange-600">
por R${" "}
{p.price.toLocaleString("pt-BR", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
</div>
{/* Parcelamento */}
<p className="text-sm text-slate-600 mt-2">
ou em 10x de R${" "}
{installmentValue.toLocaleString("pt-BR", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
</div>
{/* Informações de Estoque */}
<div className="mb-4 space-y-2 border-t border-slate-100 pt-4">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-600 font-medium">
Estoque loja
</span>
<span className="text-xs font-bold text-slate-800">
{p.stockLocal.toFixed(2) || 0} UN
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-slate-600 font-medium">
Estoque disponível
</span>
<span className="text-xs font-bold text-slate-800">
{p.stockAvailable?.toFixed(2) || p.stockLocal.toFixed(0) || 0} UN
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-slate-600 font-medium">
Estoque geral
</span>
<span className="text-xs font-bold text-slate-800">
{p.stockGeneral.toFixed(2) || 0} UN
</span>
</div>
</div>
{/* Seletor de Quantidade e Botão Adicionar */}
<div className="mt-auto pt-4 border-t border-slate-100 flex items-center gap-3">
{/* Seletor de Quantidade */}
<div className="flex items-center border border-slate-300 rounded-lg overflow-hidden">
<button
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
className="bg-[#002147] text-white px-4 py-2 hover:bg-[#003366] transition-colors font-bold"
disabled={quantity <= 1}
>
-
</button>
<input
type="number"
min="1"
value={quantity.toFixed(2)}
onChange={(e) => {
const val = parseFloat(e.target.value) || 1;
setQuantity(Math.max(1, val));
}}
className="w-20 text-center text-sm font-bold border-0 focus:outline-none focus:ring-0"
/>
<button
onClick={() => setQuantity((q) => q + 1)}
className="bg-[#002147] text-white px-4 py-2 hover:bg-[#003366] transition-colors font-bold"
>
+
</button>
</div>
{/* Botão Adicionar */}
<button
onClick={() => {
// Se tiver estoque na loja, adicionar direto com "Retira Imediata"
if (p.stockLocal && p.stockLocal > 0) {
const currentStore = authService.getStore() || "";
onAddToCart({
...p,
quantity,
stockStore: currentStore,
deliveryType: "RI", // Retira Imediata
} as OrderItem);
} else {
// Caso contrário, abrir modal para selecionar loja e tipo de entrega
setShowStoreDeliveryModal(true);
}
}}
className="flex-1 bg-orange-500 text-white py-2.5 rounded-lg font-bold uppercase text-xs tracking-wide hover:bg-orange-600 transition-all shadow-md"
>
Adicionar
</button>
</div>
</div>
{/* Modal de Zoom da Imagem */}
<ImageZoomModal
isOpen={showImageZoom}
onClose={() => setShowImageZoom(false)}
imageUrl={p.image || ""}
productName={p.name}
/>
{/* Modal de Seleção de Loja e Tipo de Entrega */}
<ProductStoreDeliveryModal
isOpen={showStoreDeliveryModal}
onClose={() => setShowStoreDeliveryModal(false)}
onConfirm={(stockStore, deliveryType) => {
// Adicionar ao carrinho com as informações selecionadas
onAddToCart({
...p,
quantity,
stockStore,
deliveryType,
} as OrderItem);
setShowStoreDeliveryModal(false);
}}
product={p}
quantity={quantity}
/>
</div>
);
};
export default ProductCard;