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,202 @@
import type { H3Event } from 'h3'
import { and, eq, inArray } from 'drizzle-orm'
import { carts, cartItems, products } from '../database/schema'
// Re-export shared types
export type { CartItemWithProduct, CartSummary } from '~/types/cart'
import type { CartItemWithProduct, CartSummary } from '~/types/cart'
/**
* Get or create a cart for the current user/session
*
* @param event - H3 event object
* @returns Cart record
*/
export async function getOrCreateCart(event: H3Event) {
const db = useDatabase()
const { user } = await getUserSession(event)
if (user) {
// Authenticated user - find or create cart by userId
let cart = await db.query.carts.findFirst({
where: eq(carts.userId, user.id),
})
if (!cart) {
// Create new cart for user
const [newCart] = await db
.insert(carts)
.values({
userId: user.id,
sessionId: '', // Empty for user carts (not used)
})
.returning()
cart = newCart
}
return cart
} else {
// Guest user - find or create cart by sessionId
const sessionId = getOrCreateSessionId(event)
let cart = await db.query.carts.findFirst({
where: and(eq(carts.sessionId, sessionId), eq(carts.userId, null)),
})
if (!cart) {
// Create new cart for guest
const [newCart] = await db
.insert(carts)
.values({
userId: null,
sessionId,
})
.returning()
cart = newCart
}
return cart
}
}
/**
* Get cart with all items and product details
*
* Automatically filters out unavailable products (inactive or out of stock)
* and removes them from the cart.
*
* @param cartId - Cart UUID
* @returns Cart summary with items, totals, and removed items
*/
export async function getCartWithItems(cartId: string): Promise<CartSummary> {
const db = useDatabase()
// Fetch cart
const cart = await db.query.carts.findFirst({
where: eq(carts.id, cartId),
})
if (!cart) {
throw createError({
statusCode: 404,
statusMessage: 'Cart not found',
})
}
// Fetch cart items with product details
const items = await db.query.cartItems.findMany({
where: eq(cartItems.cartId, cartId),
with: {
product: true,
},
})
// Separate available and unavailable items
const availableItems: CartItemWithProduct[] = []
const unavailableItemIds: string[] = []
const removedProductNames: string[] = []
for (const item of items) {
// Check if product is available
const isAvailable = item.product.active && item.product.stockQuantity >= item.quantity
if (isAvailable) {
// Add to available items with subtotal calculation
availableItems.push({
id: item.id,
cartId: item.cartId,
productId: item.productId,
quantity: item.quantity,
addedAt: item.addedAt,
product: {
id: item.product.id,
name: item.product.name,
description: item.product.description,
price: item.product.price,
stockQuantity: item.product.stockQuantity,
active: item.product.active,
category: item.product.category,
imageUrl: item.product.imageUrl,
},
subtotal: Number.parseFloat(item.product.price) * item.quantity,
})
} else {
// Mark for removal
unavailableItemIds.push(item.id)
removedProductNames.push(item.product.name)
}
}
// Remove unavailable items from cart
if (unavailableItemIds.length > 0) {
await db.delete(cartItems).where(inArray(cartItems.id, unavailableItemIds))
// Update cart's updatedAt timestamp
await db
.update(carts)
.set({ updatedAt: new Date() })
.where(eq(carts.id, cartId))
}
// Calculate total
const total = availableItems.reduce((sum, item) => sum + item.subtotal, 0)
const itemCount = availableItems.reduce((sum, item) => sum + item.quantity, 0)
return {
cart,
items: availableItems,
total,
itemCount,
...(removedProductNames.length > 0 && { removedItems: removedProductNames }),
}
}
/**
* Update cart's updated_at timestamp
*
* @param cartId - Cart UUID
*/
export async function touchCart(cartId: string): Promise<void> {
const db = useDatabase()
await db
.update(carts)
.set({ updatedAt: new Date() })
.where(eq(carts.id, cartId))
}
/**
* Check if a cart item belongs to the current user/session
*
* @param event - H3 event object
* @param cartItemId - Cart item UUID
* @returns true if item belongs to current user/session, false otherwise
*/
export async function verifyCartItemOwnership(
event: H3Event,
cartItemId: string
): Promise<boolean> {
const db = useDatabase()
const { user } = await getUserSession(event)
// Fetch cart item with cart details
const item = await db.query.cartItems.findFirst({
where: eq(cartItems.id, cartItemId),
with: {
cart: true,
},
})
if (!item) {
return false
}
// Check ownership
if (user) {
// Authenticated user - check userId match
return item.cart.userId === user.id
} else {
// Guest user - check sessionId match
const sessionId = getSessionId(event)
return sessionId !== null && item.cart.sessionId === sessionId && item.cart.userId === null
}
}