Add wishlist feature (frontend), gated by CompanySettings.wishlistEnabled
New: useWishlist.ts (fetch+optimistic-toggle hook, server-backed since a wishlist needs a logged-in customer, unlike the guest-friendly cart), WishlistButton.tsx (heart toggle, login-redirects on 401), Navbar's wishlist icon+badge (hidden below sm: — Account+Cart are the only always-visible icons on true mobile, a 3rd icon there risks the same computed nav-overflow class of bug documented in the figma-to-nextjs skill), and /konto/merkliste (list page, 404s if the feature gets disabled after a customer already has rows). Product gained numericId (the raw Payload id) alongside its existing slug id — WishlistItems.product is a real numeric relationship field, unlike cart/checkout's slug-keyed "commerce id". Backend counterpart (WishlistItems collection, CompanySettings toggle, migration) already deployed separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -357,6 +357,71 @@ export async function changeCustomerPassword(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export type WishlistItem = {
|
||||
id: number;
|
||||
productId: number;
|
||||
variant: string;
|
||||
};
|
||||
|
||||
// `variant` empty string, not undefined — matches WishlistItems.ts's own
|
||||
// defaultValue: '' so the (customer, product, variant) unique index
|
||||
// actually catches a duplicate add for a variant-less product too.
|
||||
export async function getWishlist(token: string, customerId: number): Promise<WishlistItem[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[customer][equals]": String(customerId),
|
||||
depth: "0",
|
||||
limit: "200",
|
||||
sort: "-createdAt",
|
||||
});
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/wishlist-items?${params}`, {
|
||||
headers: { Authorization: `JWT ${token}` },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data: { docs?: { id: number; product: number; variant?: string }[] } = await res.json();
|
||||
return (data.docs ?? []).map((doc) => ({ id: doc.id, productId: doc.product, variant: doc.variant ?? "" }));
|
||||
}
|
||||
|
||||
// Toggles a single (product, variant) — tries to create first; a 400 here
|
||||
// means the unique (customer, product, variant) index rejected it because
|
||||
// it already exists, so this falls back to finding + deleting that row
|
||||
// instead. Avoids a separate "is it already wishlisted" read before every
|
||||
// toggle (the common case, adding something new, only needs one request).
|
||||
export async function toggleWishlistItem(
|
||||
token: string,
|
||||
productId: number,
|
||||
variant: string,
|
||||
): Promise<{ ok: true; wishlisted: boolean } | { ok: false }> {
|
||||
const createRes = await fetch(`${PAYLOAD_URL}/api/wishlist-items`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `JWT ${token}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ product: productId, variant }),
|
||||
});
|
||||
if (createRes.ok) return { ok: true, wishlisted: true };
|
||||
|
||||
const findParams = new URLSearchParams({
|
||||
"where[product][equals]": String(productId),
|
||||
"where[variant][equals]": variant,
|
||||
depth: "0",
|
||||
limit: "1",
|
||||
});
|
||||
const findRes = await fetch(`${PAYLOAD_URL}/api/wishlist-items?${findParams}`, {
|
||||
headers: { Authorization: `JWT ${token}` },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!findRes.ok) return { ok: false };
|
||||
const found: { docs?: { id: number }[] } = await findRes.json();
|
||||
const existingId = found.docs?.[0]?.id;
|
||||
if (!existingId) return { ok: false };
|
||||
|
||||
const deleteRes = await fetch(`${PAYLOAD_URL}/api/wishlist-items/${existingId}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `JWT ${token}` },
|
||||
});
|
||||
if (!deleteRes.ok) return { ok: false };
|
||||
return { ok: true, wishlisted: false };
|
||||
}
|
||||
|
||||
// Called from app/api/account/verify-email/route.ts — no customer session
|
||||
// exists at this point (cold click from an email client), so this
|
||||
// authenticates as the service instead (see SERVICE_SECRET above).
|
||||
|
||||
Reference in New Issue
Block a user