feat: initial commit — storefront, convex backend, and shared packages
Completes the first milestone of The Pet Loft ecommerce platform: - apps/storefront: full customer-facing Next.js app with HeroUI (cart, checkout, orders, wishlist, product detail, shop, search, auth) - convex/: serverless backend with schema, queries, mutations, actions, HTTP routes, Stripe/Shippo integrations, and co-located tests - packages/types, packages/utils, packages/convex: shared workspace packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
80
convex/model/carts.ts
Normal file
80
convex/model/carts.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { MutationCtx, QueryCtx } from "../_generated/server";
|
||||
import { Id } from "../_generated/dataModel";
|
||||
|
||||
const CART_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
|
||||
type CartReadCtx = Pick<QueryCtx, "db">;
|
||||
|
||||
/**
|
||||
* Get cart by userId or sessionId (read-only). Returns null if not found.
|
||||
*/
|
||||
export async function getCart(
|
||||
ctx: CartReadCtx,
|
||||
userId?: Id<"users">,
|
||||
sessionId?: string
|
||||
) {
|
||||
if (userId) {
|
||||
return await ctx.db
|
||||
.query("carts")
|
||||
.withIndex("by_user", (q) => q.eq("userId", userId))
|
||||
.unique();
|
||||
}
|
||||
if (sessionId) {
|
||||
return await ctx.db
|
||||
.query("carts")
|
||||
.withIndex("by_session", (q) => q.eq("sessionId", sessionId))
|
||||
.unique();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing cart or create a new one. Mutation-only; use from addItem, updateItem, removeItem, clear, merge.
|
||||
* At least one of userId or sessionId must be provided.
|
||||
*/
|
||||
export async function getOrCreateCart(
|
||||
ctx: MutationCtx,
|
||||
userId?: Id<"users">,
|
||||
sessionId?: string
|
||||
): Promise<{ _id: Id<"carts">; items: { productId: Id<"products">; variantId?: Id<"productVariants">; quantity: number; price: number }[] }> {
|
||||
if (!userId && !sessionId) {
|
||||
throw new Error("Either userId or sessionId must be provided");
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const expiresAt = now + CART_EXPIRY_MS;
|
||||
|
||||
if (userId) {
|
||||
const existing = await ctx.db
|
||||
.query("carts")
|
||||
.withIndex("by_user", (q) => q.eq("userId", userId))
|
||||
.unique();
|
||||
if (existing) return existing;
|
||||
const id = await ctx.db.insert("carts", {
|
||||
userId,
|
||||
items: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
expiresAt,
|
||||
});
|
||||
const cart = await ctx.db.get(id);
|
||||
if (!cart) throw new Error("Failed to create cart");
|
||||
return cart;
|
||||
}
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("carts")
|
||||
.withIndex("by_session", (q) => q.eq("sessionId", sessionId!))
|
||||
.unique();
|
||||
if (existing) return existing;
|
||||
const id = await ctx.db.insert("carts", {
|
||||
sessionId: sessionId!,
|
||||
items: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
expiresAt,
|
||||
});
|
||||
const cart = await ctx.db.get(id);
|
||||
if (!cart) throw new Error("Failed to create cart");
|
||||
return cart;
|
||||
}
|
||||
Reference in New Issue
Block a user