feat(F-102): completed feature

This commit is contained in:
chattie
2026-08-21 12:02:15 +02:00
parent e46faa869a
commit 027cacd871
31 changed files with 510 additions and 80 deletions

View File

@@ -185,6 +185,7 @@ export default async function ProductPage({ params }: Props) {
priceCents={grossCents}
imageUrl={product.images?.[0]?.url}
available={true}
minPurchaseQty={product.minPurchaseQty ?? 1}
/>
) : (
<button

View File

@@ -9,18 +9,20 @@ interface Props {
priceCents: number;
imageUrl?: string;
available?: boolean;
minPurchaseQty?: number;
className?: string;
}
export default function AddToCartButton({
variantId, productId, productName, priceCents, imageUrl, available = true, className = '',
variantId, productId, productName, priceCents, imageUrl, available = true, minPurchaseQty = 1, className = '',
}: Props) {
const { addItem, itemCount } = useCart();
const [added, setAdded] = useState(false);
const qty = Math.max(1, minPurchaseQty);
const handleAdd = () => {
if (!available) return;
addItem({ variantId, productId, productName, quantity: 1, priceCents, imageUrl });
addItem({ variantId, productId, productName, quantity: qty, priceCents, imageUrl, minPurchaseQty: qty });
setAdded(true);
setTimeout(() => setAdded(false), 2000);
};
@@ -42,11 +44,16 @@ export default function AddToCartButton({
}
return (
<button
onClick={handleAdd}
className={`px-8 py-3.5 bg-[#70ad47] hover:bg-[#5a9040] text-white font-semibold rounded-xl transition-colors shadow-lg ${className}`}
>
Añadir al carrito
</button>
<div>
<button
onClick={handleAdd}
className={`px-8 py-3.5 bg-[#70ad47] hover:bg-[#5a9040] text-white font-semibold rounded-xl transition-colors shadow-lg ${className}`}
>
Añadir al carrito{qty > 1 ? ` (${qty} uds.)` : ''}
</button>
{qty > 1 && (
<p className="mt-2 text-xs text-gray-500">Compra mínima: {qty} unidades.</p>
)}
</div>
);
}

View File

@@ -41,7 +41,9 @@ function CartItemRow({ item }: { item: CartItem }) {
<div className="flex items-center border border-gray-300 rounded-lg">
<button
onClick={() => changeQuantity(item.variantId, item.quantity - 1)}
className="w-8 h-8 flex items-center justify-center text-gray-600 hover:text-[#70ad47] transition-colors"
disabled={item.quantity <= (item.minPurchaseQty ?? 1)}
title={item.quantity <= (item.minPurchaseQty ?? 1) && (item.minPurchaseQty ?? 1) > 1 ? `Compra mínima: ${item.minPurchaseQty} uds.` : undefined}
className="w-8 h-8 flex items-center justify-center text-gray-600 hover:text-[#70ad47] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
</button>

View File

@@ -9,9 +9,10 @@ interface Props {
priceCents: number;
imageUrl?: string;
available: boolean;
minPurchaseQty?: number;
}
export default function ProductAddToCart({ variantId, productId, productName, priceCents, imageUrl, available }: Props) {
export default function ProductAddToCart({ variantId, productId, productName, priceCents, imageUrl, available, minPurchaseQty }: Props) {
return (
<div className="mt-6">
<AddToCartButton
@@ -21,6 +22,7 @@ export default function ProductAddToCart({ variantId, productId, productName, pr
priceCents={priceCents}
imageUrl={imageUrl}
available={available}
minPurchaseQty={minPurchaseQty}
className="w-full sm:w-auto"
/>
</div>

View File

@@ -8,6 +8,8 @@ export interface CartItem {
quantity: number;
priceCents: number;
imageUrl?: string;
/** Cantidad mínima de compra del producto (F-102). */
minPurchaseQty?: number;
}
interface CartContextValue {
@@ -39,7 +41,15 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
setItems((prev) => {
const existing = prev.find((i) => i.variantId === item.variantId);
const next = existing
? prev.map((i) => i.variantId === item.variantId ? { ...i, quantity: i.quantity + item.quantity } : i)
? prev.map((i) =>
i.variantId === item.variantId
? {
...i,
quantity: i.quantity + item.quantity,
minPurchaseQty: Math.max(i.minPurchaseQty ?? 1, item.minPurchaseQty ?? 1),
}
: i,
)
: [...prev, item];
localStorage.setItem('mdv_cart', JSON.stringify(next));
return next;
@@ -56,9 +66,14 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
const changeQuantity = useCallback((variantId: string, quantity: number) => {
setItems((prev) => {
const next = quantity <= 0
? prev.filter((i) => i.variantId !== variantId)
: prev.map((i) => i.variantId === variantId ? { ...i, quantity } : i);
const next =
quantity <= 0
? prev.filter((i) => i.variantId !== variantId)
: prev.map((i) =>
i.variantId === variantId
? { ...i, quantity: Math.max(quantity, i.minPurchaseQty ?? 1) }
: i,
);
localStorage.setItem('mdv_cart', JSON.stringify(next));
return next;
});

View File

@@ -36,6 +36,8 @@ export interface Product {
priceCents?: number; // populated via separate pricing lookup
brand?: { id: string; name: string; slug: string };
imageUrl?: string;
unitWeightKg?: number;
minPurchaseQty?: number;
}
export interface Category {