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:
202
server/utils/cart-helpers.ts
Normal file
202
server/utils/cart-helpers.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user