import { query, mutation } from "./_generated/server"; import { v } from "convex/values"; import * as Users from "./model/users"; import { enrichProducts } from "./model/products"; import type { Id } from "./_generated/dataModel"; export const list = query({ args: {}, handler: async (ctx) => { const user = await Users.getCurrentUserOrThrow(ctx); const rows = await ctx.db .query("wishlists") .withIndex("by_user", (q) => q.eq("userId", user._id)) .collect(); if (rows.length === 0) return []; const productIds = [...new Set(rows.map((r) => r.productId))]; const products = ( await Promise.all(productIds.map((id) => ctx.db.get(id))) ).filter(Boolean) as Awaited>[]; const enriched = await enrichProducts(ctx, products); const productMap = new Map( enriched.map((p) => [p._id, p]), ); return rows.map((row) => { const product = productMap.get(row.productId); const variant = row.variantId && product?.variants ? product.variants.find((v: { _id: Id<"productVariants"> }) => v._id === row.variantId) : undefined; return { ...row, product, variant }; }); }, }); function findExistingEntry( rows: { variantId?: Id<"productVariants"> }[], variantId?: Id<"productVariants">, ) { return rows.find((r) => { if (variantId === undefined && r.variantId === undefined) return true; return r.variantId === variantId; }); } export const add = mutation({ args: { productId: v.id("products"), variantId: v.optional(v.id("productVariants")), notifyOnPriceDrop: v.optional(v.boolean()), notifyOnBackInStock: v.optional(v.boolean()), }, handler: async (ctx, args) => { const user = await Users.getCurrentUserOrThrow(ctx); const existing = await ctx.db .query("wishlists") .withIndex("by_user_and_product", (q) => q.eq("userId", user._id).eq("productId", args.productId), ) .collect(); const found = findExistingEntry(existing, args.variantId); if (found) return { id: (found as { _id: Id<"wishlists"> })._id, alreadyExisted: true }; let priceWhenAdded = 0; if (args.variantId) { const variant = await ctx.db.get(args.variantId); if (variant && variant.productId === args.productId) { priceWhenAdded = variant.price; } } else { const variants = await ctx.db .query("productVariants") .withIndex("by_product_and_active", (q) => q.eq("productId", args.productId).eq("isActive", true), ) .first(); if (variants) priceWhenAdded = variants.price; } const id = await ctx.db.insert("wishlists", { userId: user._id, productId: args.productId, variantId: args.variantId, addedAt: Date.now(), notifyOnPriceDrop: args.notifyOnPriceDrop ?? false, notifyOnBackInStock: args.notifyOnBackInStock ?? false, priceWhenAdded, }); return { id, alreadyExisted: false }; }, }); export const remove = mutation({ args: { id: v.id("wishlists") }, handler: async (ctx, { id }) => { const doc = await ctx.db.get(id); if (!doc) throw new Error("Wishlist item not found"); await Users.requireOwnership(ctx, doc.userId); await ctx.db.delete(id); }, }); export const toggle = mutation({ args: { productId: v.id("products"), variantId: v.optional(v.id("productVariants")), }, handler: async (ctx, args) => { const user = await Users.getCurrentUserOrThrow(ctx); const existing = await ctx.db .query("wishlists") .withIndex("by_user_and_product", (q) => q.eq("userId", user._id).eq("productId", args.productId), ) .collect(); const found = findExistingEntry(existing, args.variantId) as | { _id: Id<"wishlists"> } | undefined; if (found) { await ctx.db.delete(found._id); return { removed: true }; } const id = await ctx.db.insert("wishlists", { userId: user._id, productId: args.productId, variantId: args.variantId, addedAt: Date.now(), notifyOnPriceDrop: false, notifyOnBackInStock: false, priceWhenAdded: await (async () => { if (args.variantId) { const v = await ctx.db.get(args.variantId); return v && v.productId === args.productId ? v.price : 0; } const first = await ctx.db .query("productVariants") .withIndex("by_product_and_active", (q) => q.eq("productId", args.productId).eq("isActive", true), ) .first(); return first?.price ?? 0; })(), }); return { added: true, id }; }, }); export const count = query({ args: {}, handler: async (ctx) => { const user = await Users.getCurrentUser(ctx); if (!user) return 0; const rows = await ctx.db .query("wishlists") .withIndex("by_user", (q) => q.eq("userId", user._id)) .collect(); return rows.length; }, }); export const isWishlisted = query({ args: { productId: v.id("products"), variantId: v.optional(v.id("productVariants")), }, handler: async (ctx, args) => { const user = await Users.getCurrentUserOrThrow(ctx); const rows = await ctx.db .query("wishlists") .withIndex("by_user_and_product", (q) => q.eq("userId", user._id).eq("productId", args.productId), ) .collect(); return !!findExistingEntry(rows, args.variantId); }, });