feat(ADM-018): completed feature
This commit is contained in:
52
project/frontend/src/components/cart/AddToCartButton.tsx
Normal file
52
project/frontend/src/components/cart/AddToCartButton.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useCart } from '@/contexts/CartContext';
|
||||
|
||||
interface Props {
|
||||
variantId: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
priceCents: number;
|
||||
imageUrl?: string;
|
||||
available?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function AddToCartButton({
|
||||
variantId, productId, productName, priceCents, imageUrl, available = true, className = '',
|
||||
}: Props) {
|
||||
const { addItem, itemCount } = useCart();
|
||||
const [added, setAdded] = useState(false);
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!available) return;
|
||||
addItem({ variantId, productId, productName, quantity: 1, priceCents, imageUrl });
|
||||
setAdded(true);
|
||||
setTimeout(() => setAdded(false), 2000);
|
||||
};
|
||||
|
||||
if (!available) {
|
||||
return (
|
||||
<button disabled className={`px-8 py-3.5 bg-gray-200 text-gray-500 font-semibold rounded-xl cursor-not-allowed ${className}`}>
|
||||
Agotado
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (added) {
|
||||
return (
|
||||
<button disabled className={`px-8 py-3.5 bg-[#52B788] text-white font-semibold rounded-xl cursor-default ${className}`}>
|
||||
✓ Añadido
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
24
project/frontend/src/components/cart/CartLink.tsx
Normal file
24
project/frontend/src/components/cart/CartLink.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { useCart } from '@/contexts/CartContext';
|
||||
|
||||
export default function CartLink() {
|
||||
const { itemCount } = useCart();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/cart"
|
||||
className="relative p-2 text-gray-600 hover:text-[#70ad47] transition-colors"
|
||||
aria-label={`Carrito (${itemCount} artículos)`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
{itemCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-[#E76F51] text-white text-xs font-bold rounded-full flex items-center justify-center">
|
||||
{itemCount > 9 ? '9+' : itemCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
149
project/frontend/src/components/cart/CartPageContent.tsx
Normal file
149
project/frontend/src/components/cart/CartPageContent.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useCart, type CartItem } from '@/contexts/CartContext';
|
||||
|
||||
function formatPrice(cents: number) {
|
||||
return `€${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
function CartItemRow({ item }: { item: CartItem }) {
|
||||
const { removeItem, changeQuantity } = useCart();
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 py-4 border-b border-gray-100 last:border-0">
|
||||
{/* Image */}
|
||||
<div className="w-20 h-20 bg-gray-50 rounded-lg overflow-hidden flex-shrink-0 flex items-center justify-center">
|
||||
{item.imageUrl ? (
|
||||
<Image src={item.imageUrl} alt={item.productName} width={80} height={80} className="object-cover" />
|
||||
) : (
|
||||
<span className="text-3xl">🌿</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href={`/products/${item.productId}`} className="font-semibold text-gray-900 hover:text-[#70ad47] transition-colors line-clamp-2 text-sm">
|
||||
{item.productName}
|
||||
</Link>
|
||||
<p className="text-gray-500 text-sm mt-1">{formatPrice(item.priceCents)}/ud</p>
|
||||
|
||||
{/* Quantity controls */}
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<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"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span className="w-8 text-center text-sm font-medium">{item.quantity}</span>
|
||||
<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"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeItem(item.variantId)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors text-sm"
|
||||
>
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtotal */}
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="font-bold text-[#70ad47]">{formatPrice(item.priceCents * item.quantity)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CartPageContent() {
|
||||
const { items, subtotalCents, itemCount, clearCart } = useCart();
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
|
||||
<div className="text-6xl mb-4">🛒</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Tu carrito está vacío</h1>
|
||||
<p className="text-gray-500 mb-8">Añade productos para empezar tu pedido.</p>
|
||||
<Link href="/products" className="inline-flex items-center gap-2 px-6 py-3 bg-[#70ad47] hover:bg-[#5a9040] text-white font-semibold rounded-xl transition-colors">
|
||||
Ver productos
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900" style={{ fontFamily: 'var(--font-heading)' }}>
|
||||
Carrito de compra
|
||||
</h1>
|
||||
<p className="mt-1 text-gray-500">{itemCount} artículo{itemCount !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Items */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
{items.map((item) => (
|
||||
<CartItemRow key={item.variantId} item={item} />
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={clearCart}
|
||||
className="mt-4 text-gray-400 hover:text-red-500 text-sm transition-colors"
|
||||
>
|
||||
Vaciar carrito
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div>
|
||||
<div className="bg-gray-50 rounded-xl p-6 border border-gray-200 sticky top-24">
|
||||
<h2 className="font-bold text-gray-900 mb-4">Resumen del pedido</h2>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Subtotal ({itemCount} artículos)</span>
|
||||
<span className="font-medium">{formatPrice(subtotalCents)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Envío</span>
|
||||
<span className="text-gray-500">Calculado en checkout</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Impuestos</span>
|
||||
<span>Incluidos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 mt-4 pt-4 flex justify-between items-center">
|
||||
<span className="font-bold text-gray-900">Total</span>
|
||||
<span className="text-2xl font-bold text-[#70ad47]">{formatPrice(subtotalCents)}</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="mt-4 w-full block text-center px-6 py-3.5 bg-[#70ad47] hover:bg-[#5a9040] text-white font-semibold rounded-xl transition-colors"
|
||||
>
|
||||
Proceder al checkout
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/products"
|
||||
className="mt-2 w-full block text-center px-6 py-2 text-sm text-gray-500 hover:text-[#70ad47] transition-colors"
|
||||
>
|
||||
← Seguir comprando
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
project/frontend/src/components/cart/ProductAddToCart.tsx
Normal file
28
project/frontend/src/components/cart/ProductAddToCart.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import AddToCartButton from './AddToCartButton';
|
||||
|
||||
interface Props {
|
||||
variantId: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
priceCents: number;
|
||||
imageUrl?: string;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export default function ProductAddToCart({ variantId, productId, productName, priceCents, imageUrl, available }: Props) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<AddToCartButton
|
||||
variantId={variantId}
|
||||
productId={productId}
|
||||
productName={productName}
|
||||
priceCents={priceCents}
|
||||
imageUrl={imageUrl}
|
||||
available={available}
|
||||
className="w-full sm:w-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user