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:
2026-03-04 09:31:18 +03:00
commit cc15338ad9
361 changed files with 45005 additions and 0 deletions

80
convex/model/carts.ts Normal file
View 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;
}