Implement shopping cart functionality with UI components and API integration

- Added CartItem, CartSummary, CartEmpty, CartSidebar, and CartSheet components for managing cart display and interactions.
- Integrated useCart and useCartUI composables for cart state management and UI control.
- Implemented API endpoints for cart operations, including fetching, adding, updating, and removing items.
- Enhanced user experience with loading states and notifications using vue-sonner for cart actions.
- Configured session management for guest and authenticated users, ensuring cart persistence across sessions.

This commit completes the shopping cart feature, enabling users to add items, view their cart, and proceed to checkout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Bastian Masanek
2025-11-03 12:43:13 +01:00
parent 9d0e77fc98
commit b372e2cf78
44 changed files with 2209 additions and 123 deletions

View File

@@ -0,0 +1,44 @@
/**
* GET /api/cart
*
* Get the current user's shopping cart with all items
*
* Features:
* - Returns cart for authenticated users (by userId)
* - Returns cart for guest users (by sessionId)
* - Automatically removes unavailable products (inactive or out of stock)
* - Calculates totals and subtotals
* - Returns list of removed items if any were auto-cleaned
*
* Response:
* {
* cart: { id, userId, sessionId, createdAt, updatedAt },
* items: [{ id, product, quantity, subtotal, addedAt }],
* total: number,
* itemCount: number,
* removedItems?: string[] // Names of removed products
* }
*/
export default defineEventHandler(async (event) => {
try {
// Get or create cart for current user/session
const cart = await getOrCreateCart(event)
// Get cart with items (auto-cleans unavailable products)
const cartSummary = await getCartWithItems(cart.id)
return cartSummary
} catch (error) {
// Log error for debugging
console.error('Error fetching cart:', error)
// Return empty cart on error
return {
cart: null,
items: [],
total: 0,
itemCount: 0,
}
}
})

View File

@@ -0,0 +1,91 @@
/**
* POST /api/cart/items
*
* Add a product to the shopping cart
*
* Request Body:
* {
* productId: string (UUID)
* quantity: number (positive integer, default: 1)
* }
*
* Behavior:
* - If product already in cart, increments quantity
* - Validates product exists, is active, and has sufficient stock
* - Checks role-based visibility permissions
* - Creates cart if it doesn't exist
*
* Response:
* {
* success: true,
* message: string,
* cart: CartSummary
* }
*/
import { z } from 'zod'
import { eq, and } from 'drizzle-orm'
import { cartItems } from '../../database/schema'
// Request validation schema
const addToCartSchema = z.object({
productId: z.string().uuid('Invalid product ID'),
quantity: z.number().int().positive().default(1),
})
export default defineEventHandler(async (event) => {
// Validate request body
const body = await readBody(event)
const { productId, quantity } = await addToCartSchema.parseAsync(body)
// Validate product availability and permissions
const product = await validateProductForCart(event, productId, quantity)
// Get or create cart
const cart = await getOrCreateCart(event)
const db = await useDatabase()
// Check if product already in cart
const existingItem = await db.query.cartItems.findFirst({
where: and(
eq(cartItems.cartId, cart.id),
eq(cartItems.productId, productId)
),
})
if (existingItem) {
// Product already in cart - increment quantity
const newQuantity = existingItem.quantity + quantity
// Validate new quantity against stock
validateQuantityUpdate(newQuantity, product.stockQuantity)
// Update quantity
await db
.update(cartItems)
.set({ quantity: newQuantity })
.where(eq(cartItems.id, existingItem.id))
} else {
// Add new item to cart
await db.insert(cartItems).values({
cartId: cart.id,
productId,
quantity,
})
}
// Update cart timestamp
await touchCart(cart.id)
// Return updated cart
const cartSummary = await getCartWithItems(cart.id)
return {
success: true,
message: existingItem
? `Quantity updated to ${existingItem.quantity + quantity}`
: 'Product added to cart',
cart: cartSummary,
}
})

View File

@@ -0,0 +1,65 @@
/**
* DELETE /api/cart/items/:id
*
* Remove an item from the shopping cart
*
* Validation:
* - Cart item must exist
* - Cart item must belong to current user/session
*
* Response:
* - 204 No Content on success
* - 404 Not Found if item doesn't exist or doesn't belong to user
*/
import { z } from 'zod'
import { eq } from 'drizzle-orm'
import { cartItems } from '../../../database/schema'
// Path params validation
const pathParamsSchema = z.object({
id: z.string().uuid('Invalid cart item ID'),
})
export default defineEventHandler(async (event) => {
// Validate path params
const params = await getValidatedRouterParams(event, pathParamsSchema.parse)
const cartItemId = params.id
// Verify cart item belongs to current user/session
const hasPermission = await verifyCartItemOwnership(event, cartItemId)
if (!hasPermission) {
throw createError({
statusCode: 404,
statusMessage: 'Cart item not found',
})
}
const db = await useDatabase()
// Fetch cart item to get cart ID for timestamp update
const cartItem = await db.query.cartItems.findFirst({
where: eq(cartItems.id, cartItemId),
with: {
cart: true,
},
})
if (!cartItem) {
throw createError({
statusCode: 404,
statusMessage: 'Cart item not found',
})
}
// Delete cart item
await db.delete(cartItems).where(eq(cartItems.id, cartItemId))
// Update cart timestamp
await touchCart(cartItem.cart.id)
// Return 204 No Content
setResponseStatus(event, 204)
return null
})

View File

@@ -0,0 +1,96 @@
/**
* PATCH /api/cart/items/:id
*
* Update the quantity of a cart item
*
* Request Body:
* {
* quantity: number (positive integer)
* }
*
* Validation:
* - Cart item must exist
* - Cart item must belong to current user/session
* - Quantity must be >= 1
* - Quantity must not exceed available stock
*
* Response:
* {
* success: true,
* message: string,
* cart: CartSummary
* }
*/
import { z } from 'zod'
import { eq } from 'drizzle-orm'
import { cartItems } from '../../../database/schema'
// Request validation schema
const updateQuantitySchema = z.object({
quantity: z.number().int().min(1, 'Quantity must be at least 1'),
})
// Path params validation
const pathParamsSchema = z.object({
id: z.string().uuid('Invalid cart item ID'),
})
export default defineEventHandler(async (event) => {
// Validate path params
const params = await getValidatedRouterParams(event, pathParamsSchema.parse)
const cartItemId = params.id
// Validate request body
const body = await readBody(event)
const { quantity } = await updateQuantitySchema.parseAsync(body)
// Verify cart item belongs to current user/session
const hasPermission = await verifyCartItemOwnership(event, cartItemId)
if (!hasPermission) {
throw createError({
statusCode: 404,
statusMessage: 'Cart item not found',
})
}
const db = await useDatabase()
// Fetch cart item with product details
const cartItem = await db.query.cartItems.findFirst({
where: eq(cartItems.id, cartItemId),
with: {
product: true,
cart: true,
},
})
if (!cartItem) {
throw createError({
statusCode: 404,
statusMessage: 'Cart item not found',
})
}
// Validate quantity against stock
validateQuantityUpdate(quantity, cartItem.product.stockQuantity)
// Update quantity
await db
.update(cartItems)
.set({ quantity })
.where(eq(cartItems.id, cartItemId))
// Update cart timestamp
await touchCart(cartItem.cart.id)
// Return updated cart
const cartSummary = await getCartWithItems(cartItem.cart.id)
return {
success: true,
message: 'Quantity updated successfully',
cart: cartSummary,
}
})