shopping-cart-patterns
<!-- Loaded by nextjs-ecommerce-engineer when task involves cart state, add-to-cart, quantity updates, cart persistence, or abandoned cart -->
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
<!-- Loaded by nextjs-ecommerce-engineer when task involves cart state, add-to-cart, quantity updates, cart persistence, or abandoned cart -->
Agent definition
shopping-cart-patterns.mdShopping Cart Patterns Reference
<!-- Loaded by nextjs-ecommerce-engineer when task involves cart state, add-to-cart, quantity updates, cart persistence, or abandoned cart -->
The shopping cart is the most stateful part of any e-commerce site. The fundamental tension: guests have no server identity, authenticated users want their cart everywhere.
Cart State Architecture
**When to use:** Deciding where cart state lives. The answer depends on whether the user is authenticated.
Guest user: localStorage -> sync to DB on login
Authenticated: DB is source of truth, cached in React state
Both: Cart ID in cookie survives browser refresh without login
// lib/cart.ts — server-side cart resolution
import { cookies } from 'next/headers'
import { db } from '@/lib/db'
export async function getCart() {
const cookieStore = cookies()
const cartId = cookieStore.get('cart-id')?.value
if (!cartId) return null
return db.cart.findUnique({
where: { id: cartId },
include: {
items: {
include: { product: true },
orderBy: { createdAt: 'asc' },
},
},
})
}
export async function getOrCreateCart() {
const existing = await getCart()
if (existing) return existing
const cart = await db.cart.create({ data: {} })
// Set cart cookie — persists across page navigations
cookies().set('cart-id', cart.id, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 days
})
return cart
}---
Context vs Server State
**When to use:** Context for UI-only cart state (drawer open/closed, item count badge). Server state (React Query, SWR, or Server Actions with revalidation) for actual cart data.
// context/CartContext.tsx — UI state only, not cart data
'use client'
import { createContext, useContext, useState } from 'react'
interface CartUIState {
isOpen: boolean
openCart: () => void
closeCart: () => void
}
const CartUIContext = createContext<CartUIState | null>(null)
export function CartUIProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false)
return (
<CartUIContext.Provider value={{
isOpen,
openCart: () => setIsOpen(true),
closeCart: () => setIsOpen(false),
}}>
{children}
</CartUIContext.Provider>
)
}
export function useCartUI(): CartUIState {
const ctx = useContext(CartUIContext)
if (!ctx) throw new Error('useCartUI must be used within CartUIProvider')
return ctx
}Actual cart data comes from a Server Component via `getCart()`, not from context.
---
Optimistic Updates
**When to use:** Adding/removing items. The UI should respond instantly — don't wait for the server roundtrip to update the count or remove the item.
// components/AddToCartButton.tsx
'use client'
import { useOptimistic, useTransition } from 'react'
import { addToCartAction } from '@/actions/cart'
interface CartItem {
productId: string
quantity: number
}
interface AddToCartButtonProps {
productId: string
initialCount: number
}
export function AddToCartButton({ productId, initialCount }: AddToCartButtonProps) {
const [isPending, startTransition] = useTransition()
const [optimisticCount, addOptimistic] = useOptimistic(
initialCount,
(state: number, delta: number) => state + delta
)
function handleClick(): void {
addOptimistic(1) // Update UI immediately
startTransition(async () => {
await addToCartAction(productId) // Server mutation
})
}
return (
<button
onClick={handleClick}
disabled={isPending}
aria-busy={isPending}
>
Add to cart {optimisticCount > 0 && `(${optimisticCount})`}
</button>
)
}---
Quantity Changes
**When to use:** Cart page where users can increase/decrease quantities.
// actions/cart.ts
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
import { getOrCreateCart } from '@/lib/cart'
import { z } from 'zod'
const UpdateQuantitySchema = z.object({
productId: z.string().cuid(),
quantity: z.number().int().min(0).max(99),
})
export async function updateCartQuantity(
productId: string,
quantity: number
): Promise<{ success: boolean; error?: string }> {
const parsed = UpdateQuantitySchema.safeParse({ productId, quantity })
if (!parsed.success) {
return { success: false, error: 'Invalid quantity' }
}
const cart = await getOrCreateCart()
if (quantity === 0) {
// Remove the item
await db.cartItem.deleteMany({
where: { cartId: cart.id, productId },
})
} else {
await db.cartItem.upsert({
where: { cartId_productId: { cartId: cart.id, productId } },
update: { quantity },
create: { cartId: cart.id, productId, quantity },
})
}
revalidatePath('/cart')
return { success: true }
}---
Cart Persistence (Cookies + localStorage Strategy)
**When to use:** Hybrid approach for maximum resilience — cookie for server-side reads, localStorage for offline/fast client-side access.
// lib/cart-client.ts — client-side cart operations
const CART_STORAGE_KEY = 'cart-items-preview'
interface CartPreview {
count: number
updatedAt: number
}
export function saveCartPreview(count: number): void {
try {
const preview: CartPreview = { count, updatedAt: Date.now() }
localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(preview))
} catch {
// localStorage unavailable (private browsing, storage full) — non-fatal
}
}
export function getCartPreview(): number {
try {
const raw = localStorage.getItem(CART_STORAGE_KEY)
if (!raw) return 0
const preview = JSON.parse(raw) as CartPreview
// Treat as stale after 1 hour
if (Date.now() - preview.updatedAt > 3600_000) return 0
return preview.count
} catch {
return 0
}
}---
Abandoned Cart Detection
**When to use
Read more
Shopping Cart Patterns Reference
<!-- Loaded by nextjs-ecommerce-engineer when task involves cart state, add-to-cart, quantity updates, cart persistence, or abandoned cart -->
The shopping cart is the most stateful part of any e-commerce site. The fundamental tension: guests have no server identity, authenticated users want their cart everywhere.
Cart State Architecture
**When to use:** Deciding where cart state lives. The answer depends on whether the user is authenticated.
Guest user: localStorage -> sync to DB on login Authenticated: DB is source of truth, cached in React state Both: Cart ID in cookie survives browser refresh without login
// lib/cart.ts — server-side cart resolution
import { cookies } from 'next/headers'
import { db } from '@/lib/db'
export async function getCart() {
const cookieStore = cookies()
const cartId = cookieStore.get('cart-id')?.value
if (!cartId) return null
return db.cart.findUnique({
where: { id: cartId },
include: {
items: {
include: { product: true },
orderBy: { createdAt: 'asc' },
},
},
})
}
export async function getOrCreateCart() {
const existing = await getCart()
if (existing) return existing
const cart = await db.cart.create({ data: {} })
// Set cart cookie — persists across page navigations
cookies().set('cart-id', cart.id, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 days
})
return cart
}---
Context vs Server State
**When to use:** Context for UI-only cart state (drawer open/closed, item count badge). Server state (React Query, SWR, or Server Actions with revalidation) for actual cart data.
// context/CartContext.tsx — UI state only, not cart data
'use client'
import { createContext, useContext, useState } from 'react'
interface CartUIState {
isOpen: boolean
openCart: () => void
closeCart: () => void
}
const CartUIContext = createContext<CartUIState | null>(null)
export function CartUIProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false)
return (
<CartUIContext.Provider value={{
isOpen,
openCart: () => setIsOpen(true),
closeCart: () => setIsOpen(false),
}}>
{children}
</CartUIContext.Provider>
)
}
export function useCartUI(): CartUIState {
const ctx = useContext(CartUIContext)
if (!ctx) throw new Error('useCartUI must be used within CartUIProvider')
return ctx
}Actual cart data comes from a Server Component via `getCart()`, not from context.
---
Optimistic Updates
**When to use:** Adding/removing items. The UI should respond instantly — don't wait for the server roundtrip to update the count or remove the item.
// components/AddToCartButton.tsx
'use client'
import { useOptimistic, useTransition } from 'react'
import { addToCartAction } from '@/actions/cart'
interface CartItem {
productId: string
quantity: number
}
interface AddToCartButtonProps {
productId: string
initialCount: number
}
export function AddToCartButton({ productId, initialCount }: AddToCartButtonProps) {
const [isPending, startTransition] = useTransition()
const [optimisticCount, addOptimistic] = useOptimistic(
initialCount,
(state: number, delta: number) => state + delta
)
function handleClick(): void {
addOptimistic(1) // Update UI immediately
startTransition(async () => {
await addToCartAction(productId) // Server mutation
})
}
return (
<button
onClick={handleClick}
disabled={isPending}
aria-busy={isPending}
>
Add to cart {optimisticCount > 0 && `(${optimisticCount})`}
</button>
)
}---
Quantity Changes
**When to use:** Cart page where users can increase/decrease quantities.
// actions/cart.ts
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
import { getOrCreateCart } from '@/lib/cart'
import { z } from 'zod'
const UpdateQuantitySchema = z.object({
productId: z.string().cuid(),
quantity: z.number().int().min(0).max(99),
})
export async function updateCartQuantity(
productId: string,
quantity: number
): Promise<{ success: boolean; error?: string }> {
const parsed = UpdateQuantitySchema.safeParse({ productId, quantity })
if (!parsed.success) {
return { success: false, error: 'Invalid quantity' }
}
const cart = await getOrCreateCart()
if (quantity === 0) {
// Remove the item
await db.cartItem.deleteMany({
where: { cartId: cart.id, productId },
})
} else {
await db.cartItem.upsert({
where: { cartId_productId: { cartId: cart.id, productId } },
update: { quantity },
create: { cartId: cart.id, productId, quantity },
})
}
revalidatePath('/cart')
return { success: true }
}---
Cart Persistence (Cookies + localStorage Strategy)
**When to use:** Hybrid approach for maximum resilience — cookie for server-side reads, localStorage for offline/fast client-side access.
// lib/cart-client.ts — client-side cart operations
const CART_STORAGE_KEY = 'cart-items-preview'
interface CartPreview {
count: number
updatedAt: number
}
export function saveCartPreview(count: number): void {
try {
const preview: CartPreview = { count, updatedAt: Date.now() }
localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(preview))
} catch {
// localStorage unavailable (private browsing, storage full) — non-fatal
}
}
export function getCartPreview(): number {
try {
const raw = localStorage.getItem(CART_STORAGE_KEY)
if (!raw) return 0
const preview = JSON.parse(raw) as CartPreview
// Treat as stale after 1 hour
if (Date.now() - preview.updatedAt > 3600_000) return 0
return preview.count
} catch {
return 0
}
}---
Abandoned Cart Detection
**When to use
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

