name: "E-commerce Skills" description: "Best practices and patterns for e-commerce features" compatibility: "Next.js, Supabase" license: "MIT"
lastUpdated: "2026-02-08" ← Remove or comment out this line
activationTriggers:
Shopping Cart
- "carrito"
- "cart"
- "agregar"
- "checkout"
- "localStorage"
- "item"
- "comprar"
Analytics
- "analytics"
- "ga4"
- "gtag"
- "tracking"
- "event"
- "evento"
E-commerce Skill
🎯 Quick Reference
This skill covers shopping cart and analytics tracking - essential for e-commerce conversion tracking.
Key Principles:
- Cart uses Context API + localStorage (14-day TTL, anonymous)
- Analytics tracks key e-commerce events (add_to_cart, view_cart, remove_from_cart)
- Drawer for quick add, full page for checkout flow
- Price snapshot at time of add to detect price changes
📚 Part 1: Shopping Cart
Business Rules
Key Concepts
Carrito anónimo (no requiere autenticación)
- Persiste en localStorage (14 días TTL)
- Se pierde al limpiar datos del browser
Precio snapshot al agregar
precio_unitarioguarda precio al momento de agregar- Detecta cambios de precio y muestra warning
Stock = 0 sigue siendo válido
- "Bajo pedido" sigue aplicando
- Validación real de stock en checkout (Fase 2)
Variación requerida
- No se puede agregar producto sin seleccionar variación
- Botón "Agregar al carrito" disabled hasta seleccionar
Data Structure
CarritoItem
```typescript interface CarritoItem { id: string; // UUID cliente (crypto.randomUUID()) producto_id: string; variacion_id: string; cantidad: number; precio_unitario: number; // Snapshot al agregar agregado_at: string; // ISO timestamp } ```
Carrito
```typescript interface Carrito { items: CarritoItem[]; subtotal: number; // Calculado: sum(item.precio_unitario * item.cantidad) created_at: string; updated_at: string; } ```
CarritoWithDetails
```typescript interface CarritoWithDetails extends Carrito { items: (CarritoItem & { producto: ProductoCompleto; variacion: Variacion; })[]; } ```
LocalStorage Manager
Location: `lib/storage/carrito.ts`
Key Functions
```typescript const CARRITO_KEY = "fira_carrito"; const CARRITO_TTL = 14 _ 24 _ 60 _ 60 _ 1000; // 14 días
// CRUD Operations export function getCarrito(): Carrito; export function setCarrito(carrito: Carrito): void; export function addItem(item: CarritoItem): void; export function updateItemQuantity(itemId: string, cantidad: number): void; export function removeItem(itemId: string): void; export function clearCarrito(): void;
// Helpers export function calculateSubtotal(items: CarritoItem[]): number; export function isExpired(carrito: Carrito): boolean; export function mergeItems( existing: CarritoItem, nuevo: CarritoItem, ): CarritoItem; ```
Error Handling
```typescript try { localStorage.setItem(CARRITO_KEY, JSON.stringify(carrito)); } catch (error) { // QuotaExceededError, Safari private mode console.error("localStorage error:", error); // Fallback: usar estado en memoria (se pierde al recargar) return getInMemoryCarrito(); } ```
Context API
Location: `lib/context/CarritoContext.tsx`
Interface
```typescript interface CarritoContextValue { carrito: Carrito; carritoWithDetails: CarritoWithDetails | null; itemCount: number; isLoading: boolean;
addItem: ( producto: ProductoCompleto, variacion: Variacion, cantidad: number, ) => void; updateQuantity: (itemId: string, cantidad: number) => void; removeItem: (itemId: string) => void; clearCarrito: () => void;
// Drawer control isDrawerOpen: boolean; openDrawer: () => void; closeDrawer: () => void; } ```
Provider Setup
```typescript // app/layout.tsx import { CarritoProvider } from '@/lib/context/CarritoContext';
export default function RootLayout({ children }) { return (
Usage in Components
```typescript "use client"; import { useCarrito } from "@/lib/context/CarritoContext";
export function AddToCartButton() { const { addItem, openDrawer } = useCarrito();
const handleAdd = () => { addItem(producto, variacion, 1); openDrawer(); // Abre drawer automáticamente }; } ```
Component Patterns
CartBadge (Header)
Location: `components/carrito/CartBadge.tsx`
```typescript 'use client'; import { useCarrito } from '@/lib/context/CarritoContext';
export function CartBadge() { const { itemCount, openDrawer } = useCarrito();
return ( ); } ```
AddToCartButton
Location: `components/carrito/AddToCartButton.tsx`
```typescript 'use client';
interface AddToCartButtonProps { producto: ProductoCompleto; variacion: Variacion | null; cantidad?: number; }
export function AddToCartButton({ producto, variacion, cantidad = 1 }) { const { addItem, openDrawer } = useCarrito(); const [isAdding, setIsAdding] = useState(false);
const handleClick = async () => { if (!variacion) return;
setIsAdding(true);
trackAddToCart(producto, variacion, cantidad);
addItem(producto, variacion, cantidad);
openDrawer();
setTimeout(() => setIsAdding(false), 1000);
};
return ( <Button onClick={handleClick} disabled={!variacion || isAdding} > {isAdding ? '✓ Agregado' : 'Agregar al carrito'} ); } ```
CartItemCard
Location: `components/carrito/CartItemCard.tsx`
```typescript interface CartItemCardProps { item: CarritoItem & { producto: ProductoCompleto; variacion: Variacion; }; onUpdateQuantity: (itemId: string, cantidad: number) => void; onRemove: (itemId: string) => void; compact?: boolean; // true para drawer, false para página }
export function CartItemCard({ item, onUpdateQuantity, onRemove, compact }) { const imagenPrincipal = item.producto.imagenes.find(i => i.es_principal); const priceChanged = item.precio_unitario !== item.variacion.precio;
return (
<div className={COMPONENTS.carrito.item.content}>
<h3>{item.producto.nombre}</h3>
<p>{item.variacion.tamanio} • {item.variacion.color}</p>
{priceChanged && (
<p className="text-warning">
Precio cambió: {formatPrice(item.variacion.precio)}
</p>
)}
{!compact && (
<QuantitySelector
value={item.cantidad}
onChange={(val) => onUpdateQuantity(item.id, val)}
/>
)}
<p>{formatPrice(item.precio_unitario * item.cantidad)}</p>
</div>
<button onClick={() => onRemove(item.id)}>
🗑️
</button>
</div>
); } ```
CartDrawer
Location: `components/carrito/CartDrawer.tsx`
```typescript 'use client';
export function CartDrawer() { const { carritoWithDetails, isDrawerOpen, closeDrawer, itemCount } = useCarrito();
useScrollLock(isDrawerOpen); useEscapeKey(closeDrawer, isDrawerOpen);
if (!isDrawerOpen) return null;
return ( <> {/_ Overlay _/}
{/* Drawer */}
<div className={COMPONENTS.carrito.drawer.base}>
<div className={COMPONENTS.carrito.drawer.header}>
<h2>{CARRITO_CONTENT.drawer.title} ({itemCount})</h2>
<button onClick={closeDrawer}>✕</button>
</div>
<div className={COMPONENTS.carrito.drawer.content}>
{itemCount === 0 ? (
<EmptyCart compact />
) : (
carritoWithDetails?.items.map(item => (
<CartItemCard key={item.id} item={item} compact />
))
)}
</div>
<div className={COMPONENTS.carrito.drawer.footer}>
<div className="flex justify-between">
<span>{CARRITO_CONTENT.drawer.subtotal}</span>
<span>{formatPrice(carritoWithDetails?.subtotal)}</span>
</div>
<Button href="/carrito" size="lg">
{CARRITO_CONTENT.drawer.checkout}
</Button>
</div>
</div>
</>
); } ```
Validation Rules
Al Agregar
```typescript function validateAddToCart(variacion: Variacion | null): string | null { if (!variacion) { return "Seleccioná una variación"; }
if (!variacion.activo) { return "Esta variación no está disponible"; }
return null; // Valid } ```
En Carrito
```typescript function validateCartItem( item: CarritoItem, producto: ProductoCompleto, variacion: Variacion, ): { isValid: boolean; warnings: string[]; } { const warnings: string[] = [];
if (!producto.activo) { warnings.push(CARRITO_CONTENT.page.validation.productInactive); }
if (!variacion.activo) { warnings.push(CARRITO_CONTENT.page.validation.variationInactive); }
if (item.precio_unitario !== variacion.precio) { warnings.push(CARRITO_CONTENT.page.validation.priceChanged); }
return { isValid: warnings.length === 0, warnings, }; } ```
Merging Items
Si usuario agrega mismo producto+variación 2 veces:
```typescript function mergeItems(existing: CarritoItem, nuevo: CarritoItem): CarritoItem { // Sumar cantidades return { ...existing, cantidad: existing.cantidad + nuevo.cantidad, updated_at: new Date().toISOString(), }; } ```
📚 Part 2: Analytics Tracking
Setup
Environment Variable:
```env NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX ```
Architecture:
```
app/layout.tsx
└─
lib/analytics/gtag.ts ├─ trackWhatsAppClick() ├─ trackProductView() ├─ trackCategoryFilter() ├─ trackVariationSelect() ├─ trackAddToCart() ← Cart Event ├─ trackViewCart() ← Cart Event └─ trackRemoveFromCart() ← Cart Event ```
Tracked Events
Products (V1)
- whatsapp_click - Usuario hace clic en botón WhatsApp
- view_item - Usuario visualiza producto
- filter_products - Usuario filtra por categoría
- select_item - Usuario selecciona variación
Cart (V2 Phase 1)
- add_to_cart - Usuario agrega item al carrito
- view_cart - Usuario abre carrito (drawer o página)
- remove_from_cart - Usuario elimina item del carrito
Cart Event Tracking
1. add_to_cart
Se dispara cuando el usuario agrega un ítem al carrito.
Parámetros:
- `event_category`: "ecommerce"
- `producto_id`: UUID del producto
- `producto_nombre`: Nombre del producto
- `variacion_id`: UUID de la variación
- `variacion_tamanio`: Size seleccionado
- `variacion_color`: Color seleccionado
- `cantidad`: Items agregados
- `precio_unitario`: Precio snapshot al momento de agregar
- `value`: precio_unitario * cantidad
Implementation:
```typescript /**
- Track add to cart event
- @param producto - Product being added
- @param variacion - Selected variation
- @param cantidad - Quantity added
- @param precio_unitario - Price snapshot at time of add */ export function trackAddToCart( producto: Producto, variacion: Variacion, cantidad: number, precio_unitario: number, ): void { if (!canTrack()) return;
const value = precio_unitario * cantidad;
window.gtag!("event", "add_to_cart", { event_category: "ecommerce", producto_id: producto.id, producto_nombre: producto.nombre, variacion_id: variacion.id, variacion_tamanio: variacion.tamanio, variacion_color: variacion.color, cantidad, precio_unitario, value, }); } ```
2. view_cart
Se dispara al abrir la página de carrito o el drawer.
Parámetros:
- `event_category`: "ecommerce"
- `item_count`: Cantidad total de items
- `subtotal`: Valor total del carrito
- `value`: subtotal
Implementation:
```typescript /**
- Track view cart event
- @param item_count - Number of items in cart
- @param subtotal - Total cart value */ export function trackViewCart(item_count: number, subtotal: number): void { if (!canTrack()) return;
window.gtag!("event", "view_cart", { event_category: "ecommerce", item_count, subtotal, value: subtotal, }); } ```
3. remove_from_cart
Se dispara cuando se elimina un ítem del carrito.
Parámetros:
- `event_category`: "ecommerce"
- `producto_id`: UUID del producto removido
- `variacion_id`: UUID de la variación removida
- `cantidad`: Items removidos
- `value`: Valor total del item removido
Implementation:
```typescript /**
- Track remove from cart event
- @param producto - Product being removed
- @param variacion - Removed variation
- @param cantidad - Quantity removed
- @param value - Total value of removed item */ export function trackRemoveFromCart( producto: Producto, variacion: Variacion, cantidad: number, value: number, ): void { if (!canTrack()) return;
window.gtag!("event", "remove_from_cart", { event_category: "ecommerce", producto_id: producto.id, variacion_id: variacion.id, cantidad, value, }); } ```
Debugging
- DevTools → Network → filtrar "google-analytics"
- GA4 DebugView (con extensión Google Analytics Debugger)
- En dev: GA4 no se carga (verificar con console.log)
Common Issues:
- ID mal configurado
- No estás en producción
- Adblockers bloqueando requests
- DevTools Network → Check para gtag requests
🔗 Integration Examples
Complete Add to Cart Flow
```typescript 'use client'; import { useCarrito } from '@/lib/context/CarritoContext'; import { trackAddToCart } from '@/lib/analytics/gtag';
export function ProductDetailContent({ producto }) { const [selectedVariacion, setSelectedVariacion] = useState<Variacion | null>(null); const { addItem, openDrawer } = useCarrito(); const [isAdding, setIsAdding] = useState(false);
const handleAddToCart = () => { if (!selectedVariacion) return;
setIsAdding(true);
// 1. Add to cart
addItem(producto, selectedVariacion, 1);
// 2. Track analytics event
trackAddToCart(
producto,
selectedVariacion,
1,
selectedVariacion.precio
);
// 3. Open drawer for confirmation
openDrawer();
setTimeout(() => setIsAdding(false), 1000);
};
return (
<Button
onClick={handleAddToCart}
disabled={!selectedVariacion || isAdding}
>
{isAdding ? '✓ Agregado' : 'Agregar al carrito'}
</Button>
</div>
); } ```
Cart Drawer with Analytics
```typescript 'use client'; import { useCarrito } from '@/lib/context/CarritoContext'; import { trackViewCart } from '@/lib/analytics/gtag'; import { useEffect } from 'react';
export function CartDrawer() { const { carritoWithDetails, isDrawerOpen, closeDrawer, itemCount } = useCarrito();
// Track view_cart when drawer opens useEffect(() => { if (isDrawerOpen && itemCount > 0 && carritoWithDetails) { trackViewCart(itemCount, carritoWithDetails.subtotal); } }, [isDrawerOpen, itemCount, carritoWithDetails]);
if (!isDrawerOpen) return null;
return (
Remove from Cart with Analytics
```typescript 'use client'; import { useCarrito } from '@/lib/context/CarritoContext'; import { trackRemoveFromCart } from '@/lib/analytics/gtag';
export function CartItemCard({ item, producto, variacion }) { const { removeItem } = useCarrito();
const handleRemove = () => { const itemValue = item.precio_unitario * item.cantidad;
// 1. Remove from cart
removeItem(item.id);
// 2. Track analytics event
trackRemoveFromCart(
producto,
variacion,
item.cantidad,
itemValue
);
};
return (
Cart Page with Analytics
```typescript 'use client'; import { useCarrito } from '@/lib/context/CarritoContext'; import { trackViewCart } from '@/lib/analytics/gtag'; import { useEffect } from 'react';
export function CarritoContent() { const { carritoWithDetails, itemCount } = useCarrito();
// Track view_cart when page loads useEffect(() => { if (itemCount > 0 && carritoWithDetails) { trackViewCart(itemCount, carritoWithDetails.subtotal); } }, []); // Only on mount
if (itemCount === 0) {
return
return (
<div className="lg:col-span-1">
<CartSummary carrito={carritoWithDetails} />
</div>
</div>
); } ```
📚 Related Documentation
- Business logic: `.github/reference/business-logic.md` (Cart Lifecycle)
- Analytics events: `lib/analytics/events.ts`
- Content: `lib/content/carrito.ts`
- Design tokens: `lib/design/tokens.ts` (COMPONENTS.carrito)
- Google Analytics 4 Documentation
✅ Best Practices Checklist
Shopping Cart:
- Validar variación seleccionada antes de agregar
- Snapshot precio al agregar (`precio_unitario`)
- Detectar cambios de precio y mostrar warning
- Mergear items duplicados (mismo producto+variación)
- Handle localStorage errors (Safari private mode)
- TTL de 14 días para expiración
- Focus trap en drawer (useEscapeKey, useScrollLock)
- Empty states con CTAs claros
- Loading states en botones (isAdding)
Analytics:
- `NEXT_PUBLIC_GA_MEASUREMENT_ID` configurado
- Eventos visibles en DebugView
- Tracking activo solo en prod
- add_to_cart se dispara al agregar producto
- view_cart se dispara al abrir drawer/página
- remove_from_cart se dispara al remover item
- item_count y subtotal correctos
- price snapshots guardados
- Test en DebugView → 24-48h para standard reports