From 0c72b70de724fc29d61e90b962a0909548ecb960 Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 19 Jul 2026 23:22:26 +0000 Subject: [PATCH] fix(cart): pad RelatedProducts to 3 cards even on first pick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog only has 4 products; with 2 already in the cart, the initial pick's pool shrinks to 2 and pickRandom's slice can't pad past what's available, so the grid rendered 2 cards instead of 3. The swap-after-add path already had fallback logic for this exact case — extracted it into pickWithFallback() and reused it for the initial pick too. --- app/cart/components/RelatedProducts.tsx | 42 ++++++++++++++----------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/app/cart/components/RelatedProducts.tsx b/app/cart/components/RelatedProducts.tsx index de1d1d1..26d6bff 100644 --- a/app/cart/components/RelatedProducts.tsx +++ b/app/cart/components/RelatedProducts.tsx @@ -16,6 +16,27 @@ function pickRandom(allIds: string[], excludeIds: string[], count: number): stri return shuffled.slice(0, count); } +// The catalog only has a handful of products — once the cart holds enough +// distinct ones, "N recommendations that aren't already in the cart" can +// become impossible (e.g. 4 products total, 2 already in the cart, but +// DISPLAY_COUNT is 3 — only 2 non-cart products exist, period). Falling +// back to re-suggesting something already in the cart (a normal "grab +// another one" pattern) beats silently shrinking the grid below +// DISPLAY_COUNT. Shared by both the initial pick and the swap-after-add +// path so neither can under-fill the grid. +function pickWithFallback(allIds: string[], excludeIds: string[], keep: string[], count: number): string[] { + const missing = count - keep.length; + if (missing <= 0) return keep; + + let picks = pickRandom(allIds, [...excludeIds, ...keep], missing); + if (picks.length < missing) { + const stillMissing = missing - picks.length; + const fallback = pickRandom(allIds, [...keep, ...picks], stillMissing); + picks = [...picks, ...fallback]; + } + return [...keep, ...picks]; +} + export function RelatedProducts() { const cart = useCart(); const products = useProducts(); @@ -55,7 +76,7 @@ export function RelatedProducts() { if (!pickedRef.current) { pickedRef.current = true; - setDisplayIds(pickRandom(productIds, cartIds, DISPLAY_COUNT)); + setDisplayIds(pickWithFallback(productIds, cartIds, [], DISPLAY_COUNT)); return; } @@ -67,24 +88,7 @@ export function RelatedProducts() { swapTimeoutRef.current = setTimeout(() => { setDisplayIds((prev) => { const stillRelevant = prev.filter((id) => !cartIds.includes(id)); - const missing = DISPLAY_COUNT - stillRelevant.length; - if (missing <= 0) return stillRelevant; - - let replacements = pickRandom(productIds, [...cartIds, ...stillRelevant], missing); - - // The catalog only has a handful of products — once the cart - // holds enough distinct ones, "N recommendations that aren't - // already in the cart" can become impossible. Falling back to - // re-suggesting something already in the cart (a normal "grab - // another one" pattern) beats silently shrinking the grid below - // DISPLAY_COUNT. - if (replacements.length < missing) { - const stillMissing = missing - replacements.length; - const fallback = pickRandom(productIds, [...stillRelevant, ...replacements], stillMissing); - replacements = [...replacements, ...fallback]; - } - - return [...stillRelevant, ...replacements]; + return pickWithFallback(productIds, cartIds, stillRelevant, DISPLAY_COUNT); }); }, FEEDBACK_MS); }, [cartKey, productIds]);