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

View File

@@ -0,0 +1,99 @@
"use client";
import { useConvexAuth } from "convex/react";
import { useState, useCallback } from "react";
import { toast } from "@heroui/react";
import { useWishlist } from "@/lib/wishlist/useWishlist";
import { useWishlistMutations } from "@/lib/wishlist/useWishlistMutations";
import type { WishlistItem } from "@/lib/wishlist/types";
import { WishlistItemGrid } from "./WishlistItemGrid";
import { RemoveFromWishlistDialog } from "./RemoveFromWishlistDialog";
import { WishlistSkeleton } from "./state/WishlistSkeleton";
import { WishlistEmptyState } from "./state/WishlistEmptyState";
import { WishlistSignInPrompt } from "./state/WishlistSignInPrompt";
export function WishlistPageView() {
const { isAuthenticated, isLoading: authLoading } = useConvexAuth();
const { items, isLoading, isEmpty } = useWishlist();
const { removeItem, isRemoving, addToCart, isAddingToCart } =
useWishlistMutations();
const [removeTarget, setRemoveTarget] = useState<WishlistItem | null>(null);
const [removingId, setRemovingId] = useState<string | null>(null);
const [addingToCartId, setAddingToCartId] = useState<string | null>(null);
const handleRemove = useCallback(
(id: string) => {
const target = items.find((i) => i._id === id);
setRemoveTarget(target ?? null);
},
[items],
);
const handleConfirmRemove = useCallback(async () => {
if (!removeTarget) return;
const name = removeTarget.product?.name ?? "Item";
setRemovingId(removeTarget._id);
try {
await removeItem(removeTarget._id);
toast.success(`${name} removed from wishlist`);
} catch {
toast.danger("Failed to remove item");
} finally {
setRemovingId(null);
setRemoveTarget(null);
}
}, [removeTarget, removeItem]);
const handleAddToCart = useCallback(
async (variantId: string) => {
const item = items.find(
(i) =>
i.variant?._id === variantId ||
i.product?.variants[0]?._id === variantId,
);
setAddingToCartId(item?._id ?? null);
try {
await addToCart(variantId, 1);
toast.success("Added to cart");
} catch {
toast.danger("This item is currently out of stock");
} finally {
setAddingToCartId(null);
}
},
[items, addToCart],
);
if (!authLoading && !isAuthenticated) {
return <WishlistSignInPrompt />;
}
if (authLoading || isLoading) {
return <WishlistSkeleton />;
}
if (isEmpty) {
return <WishlistEmptyState />;
}
return (
<>
<WishlistItemGrid
items={items}
onRemove={handleRemove}
onAddToCart={handleAddToCart}
removingId={removingId}
addingToCartId={addingToCartId}
/>
<RemoveFromWishlistDialog
isOpen={!!removeTarget}
onClose={() => setRemoveTarget(null)}
onConfirm={handleConfirmRemove}
isRemoving={isRemoving}
productName={removeTarget?.product?.name ?? "this item"}
/>
</>
);
}