feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}