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:
168
convex/addresses.ts
Normal file
168
convex/addresses.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import * as Users from "./model/users";
|
||||
|
||||
const addressTypeValidator = v.union(
|
||||
v.literal("shipping"),
|
||||
v.literal("billing"),
|
||||
);
|
||||
|
||||
const addressFieldsValidator = {
|
||||
type: addressTypeValidator,
|
||||
fullName: v.optional(v.string()),
|
||||
firstName: v.string(),
|
||||
lastName: v.string(),
|
||||
phone: v.string(),
|
||||
addressLine1: v.string(),
|
||||
additionalInformation: v.optional(v.string()),
|
||||
city: v.string(),
|
||||
postalCode: v.string(),
|
||||
country: v.string(),
|
||||
isDefault: v.boolean(),
|
||||
isValidated: v.optional(v.boolean()),
|
||||
};
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const user = await Users.getCurrentUserOrThrow(ctx);
|
||||
const addresses = await ctx.db
|
||||
.query("addresses")
|
||||
.withIndex("by_user", (q) => q.eq("userId", user._id))
|
||||
.collect();
|
||||
addresses.sort((a, b) => {
|
||||
if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
|
||||
return b._creationTime - a._creationTime;
|
||||
});
|
||||
return addresses;
|
||||
},
|
||||
});
|
||||
|
||||
export const add = mutation({
|
||||
args: addressFieldsValidator,
|
||||
handler: async (ctx, args) => {
|
||||
const user = await Users.getCurrentUserOrThrow(ctx);
|
||||
|
||||
const existingOfType = await ctx.db
|
||||
.query("addresses")
|
||||
.withIndex("by_user_and_type", (q) =>
|
||||
q.eq("userId", user._id).eq("type", args.type),
|
||||
)
|
||||
.collect();
|
||||
|
||||
const isDefault = existingOfType.length === 0 ? true : args.isDefault;
|
||||
|
||||
if (isDefault && existingOfType.length > 0) {
|
||||
for (const addr of existingOfType) {
|
||||
await ctx.db.patch(addr._id, { isDefault: false });
|
||||
}
|
||||
}
|
||||
|
||||
return await ctx.db.insert("addresses", {
|
||||
userId: user._id,
|
||||
type: args.type,
|
||||
fullName: args.fullName || `${args.firstName} ${args.lastName}`,
|
||||
firstName: args.firstName,
|
||||
lastName: args.lastName,
|
||||
phone: args.phone,
|
||||
addressLine1: args.addressLine1,
|
||||
additionalInformation: args.additionalInformation,
|
||||
city: args.city,
|
||||
postalCode: args.postalCode,
|
||||
country: args.country,
|
||||
isDefault,
|
||||
isValidated: args.isValidated ?? false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
id: v.id("addresses"),
|
||||
type: v.optional(addressTypeValidator),
|
||||
fullName: v.optional(v.string()),
|
||||
firstName: v.optional(v.string()),
|
||||
lastName: v.optional(v.string()),
|
||||
phone: v.optional(v.string()),
|
||||
addressLine1: v.optional(v.string()),
|
||||
additionalInformation: v.optional(v.string()),
|
||||
city: v.optional(v.string()),
|
||||
postalCode: v.optional(v.string()),
|
||||
country: v.optional(v.string()),
|
||||
isDefault: v.optional(v.boolean()),
|
||||
isValidated: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...updates } = args;
|
||||
const address = await ctx.db.get(id);
|
||||
if (!address) throw new Error("Address not found");
|
||||
await Users.requireOwnership(ctx, address.userId);
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (updates.type !== undefined) patch.type = updates.type;
|
||||
if (updates.fullName !== undefined) patch.fullName = updates.fullName;
|
||||
if (updates.firstName !== undefined) patch.firstName = updates.firstName;
|
||||
if (updates.lastName !== undefined) patch.lastName = updates.lastName;
|
||||
if (updates.phone !== undefined) patch.phone = updates.phone;
|
||||
if (updates.addressLine1 !== undefined)
|
||||
patch.addressLine1 = updates.addressLine1;
|
||||
if (updates.additionalInformation !== undefined)
|
||||
patch.additionalInformation = updates.additionalInformation;
|
||||
if (updates.city !== undefined) patch.city = updates.city;
|
||||
if (updates.postalCode !== undefined) patch.postalCode = updates.postalCode;
|
||||
if (updates.country !== undefined) patch.country = updates.country;
|
||||
if (updates.isDefault !== undefined) patch.isDefault = updates.isDefault;
|
||||
if (updates.isValidated !== undefined)
|
||||
patch.isValidated = updates.isValidated;
|
||||
if (Object.keys(patch).length === 0) return id;
|
||||
await ctx.db.patch(id, patch as any);
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: { id: v.id("addresses") },
|
||||
handler: async (ctx, { id }) => {
|
||||
const address = await ctx.db.get(id);
|
||||
if (!address) throw new Error("Address not found");
|
||||
await Users.requireOwnership(ctx, address.userId);
|
||||
if (address.isDefault) {
|
||||
const rest = await ctx.db
|
||||
.query("addresses")
|
||||
.withIndex("by_user", (q) => q.eq("userId", address.userId))
|
||||
.filter((q) => q.neq(q.field("_id"), id))
|
||||
.collect();
|
||||
const first = rest[0];
|
||||
if (first) await ctx.db.patch(first._id, { isDefault: true });
|
||||
}
|
||||
await ctx.db.delete(id);
|
||||
},
|
||||
});
|
||||
|
||||
export const setDefault = mutation({
|
||||
args: { id: v.id("addresses") },
|
||||
handler: async (ctx, { id }) => {
|
||||
const address = await ctx.db.get(id);
|
||||
if (!address) throw new Error("Address not found");
|
||||
await Users.requireOwnership(ctx, address.userId);
|
||||
const all = await ctx.db
|
||||
.query("addresses")
|
||||
.withIndex("by_user", (q) => q.eq("userId", address.userId))
|
||||
.collect();
|
||||
for (const addr of all) {
|
||||
await ctx.db.patch(addr._id, { isDefault: addr._id === id });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const markValidated = mutation({
|
||||
args: {
|
||||
id: v.id("addresses"),
|
||||
isValidated: v.boolean(),
|
||||
},
|
||||
handler: async (ctx, { id, isValidated }) => {
|
||||
const address = await ctx.db.get(id);
|
||||
if (!address) throw new Error("Address not found");
|
||||
await Users.requireOwnership(ctx, address.userId);
|
||||
await ctx.db.patch(id, { isValidated });
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user