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>
100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
"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"}
|
|
/>
|
|
</>
|
|
);
|
|
}
|