feat(catalog): hover image — main → set crossfade no card de produto#598
Conversation
DB (já aplicado no Supabase doufsxqlfjyuvxuezpln): - Fix SPOT is_primary: 'main' vira primária (era 'set') — 1.174 produtos - Reclassifica XBZ d1 gallery → set (2.560 produtos) - ADD COLUMN products.set_image_url + backfill (4.086 produtos, 67%) - trigger trg_sync_set_image_url + suffix mapping XBZ d1 - Cobertura: 4.086/6.086 produtos com hover Frontend: - LightweightProduct: +set_image_url field no interface e no SELECT local - PRODUCT_SELECT_LIGHTWEIGHT: +set_image_url no SELECT do catálogo - mapLightweightToProduct: mapeia set_image_url → Product.set_image_url - Product interface: +set_image_url optional field - NEW src/components/catalog/ProductCardImage.tsx: crossfade CSS puro
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdição de set_image_url ao modelo Product para exibição de imagem alternativa no catálogo, novo componente ProductCardImage com crossfade CSS, integração no hook de produtos lightweight, remoção de sortBy da API de filtros, e simplificação de carregamento de categorias sem singleton. ChangesImagens de produto com set_image_url e simplificação de catálogo
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 332e7e8560
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| aspect?: string; | ||
| } | ||
|
|
||
| export function ProductCardImage({ |
There was a problem hiding this comment.
Wire the hover image into the rendered card
This new crossfade component is not imported by the catalog card path: src/components/products/ProductCard.tsx imports ./ProductCardImage, which resolves to src/components/products/ProductCardImage.tsx, and repo search only finds this src/components/catalog/ProductCardImage.tsx referenced inside its own file. As a result, products with set_image_url still render through the existing OptimizedImage component and never show the hover crossfade; please move this behavior into the rendered product card image component or update the import path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Este PR adiciona suporte a imagem de hover (“set image”) no catálogo, trazendo set_image_url do banco no SELECT “lightweight” e criando um componente de card com crossfade CSS, preparando o frontend para trocar a imagem ao passar o mouse sem queries adicionais.
Changes:
- Adiciona
set_image_urlaos tipos do catálogo e ao payload “lightweight” (SELECT + interface). - Inclui
set_image_urlno hook do catálogo (useProductsCatalog) e mapeia paraProduct. - Cria
src/components/catalog/ProductCardImage.tsxpara renderizar imagem principal + hover com crossfade.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/types/product-catalog.ts | Adiciona set_image_url ao tipo Product e altera ProductFilters. |
| src/lib/external-db/products-lightweight.ts | Inclui set_image_url no SELECT “lightweight” e no tipo retornado. |
| src/hooks/products/useProductsLightweight.ts | Passa set_image_url no SELECT do catálogo e mapeia no Product; altera assinatura/fluxo do hook. |
| src/components/catalog/ProductCardImage.tsx | Novo componente para crossfade CSS entre mainUrl e setUrl. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export interface ProductFilters { | ||
| category?: string; | ||
| categoryId?: string | number; | ||
| search?: string; | ||
| minPrice?: number; | ||
| maxPrice?: number; | ||
| inStock?: boolean; | ||
| limit?: number; | ||
| sortBy?: string; | ||
| } |
| export function useProductsCatalog(filters?: { | ||
| search?: string; | ||
| categories?: string[]; | ||
| suppliers?: string[]; | ||
| sortBy?: string; | ||
| }) { |
| async function loadCategoriesMap(): Promise<ReadonlyMap<string, string>> { | ||
| if (!categoriesMapPromise) { | ||
| categoriesMapPromise = fetchPromobrindCategories() | ||
| .then((categories) => new Map(categories.map((c) => [String(c.id), c.name])) as ReadonlyMap<string, string>) | ||
| .catch(() => { | ||
| categoriesMapPromise = null; // allow retry on next request | ||
| return new Map() as ReadonlyMap<string, string>; | ||
| }); | ||
| try { | ||
| const categories = await fetchPromobrindCategories(); | ||
| return new Map(categories.map((c) => [String(c.id), c.name])); | ||
| } catch { | ||
| return new Map(); | ||
| } | ||
| return categoriesMapPromise; | ||
| } |
| function toCfUrl(url: string | null | undefined): string | null { | ||
| if (!url) return null; | ||
| if ( | ||
| url.startsWith('https://imagedelivery.net/') && | ||
| !url.match(/\/(public|thumbnail|small|medium|large)$/) | ||
| ) { | ||
| return url + CF_PUBLIC; | ||
| } | ||
| return url; | ||
| } |
| <img | ||
| src={setSrc} | ||
| alt={`${alt} — todas as cores`} | ||
| loading="lazy" | ||
| decoding="async" |
| onError={(e) => { | ||
| // Se imagem set falhar, esconde para evitar broken image | ||
| (e.currentTarget as HTMLImageElement).style.display = 'none'; | ||
| }} |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/catalog/ProductCardImage.tsx`:
- Around line 106-109: The onError handler in ProductCardImage.tsx currently
always sets (e.currentTarget as HTMLImageElement).src = '/placeholder.svg' which
can cause an infinite loop if the placeholder fails; modify the onError handler
to first check the current image src or a flag (e.g., img.dataset['errored'] or
compare e.currentTarget.src) and only set src to '/placeholder.svg' if it isn’t
already the placeholder (and mark the image as errored to prevent repeated
retries), updating the onError in the ProductCardImage component accordingly.
In `@src/hooks/products/useProductsLightweight.ts`:
- Around line 112-119: loadCategoriesMap currently calls
fetchPromobrindCategories on every queryFn run (triggered by fetchCatalogPage in
useProductsCatalog/useInfiniteQuery) and does not short‑circuit when
immutableCache exists, causing repeated network/dbInvoke; memoize the categories
promise or result at hook/module scope (or reuse the existing useCategories
React Query hook with a long staleTime) and change loadCategoriesMap to return
the cached Promise/Map (or early‑return when immutableCache present after
checking putInCacheSafe) so mapLightweightToProduct and fetchCatalogPage reuse
the single categoriesById instance instead of refetching on each page load.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f36aca60-7319-4472-bd4e-216f997857a0
📒 Files selected for processing (4)
src/components/catalog/ProductCardImage.tsxsrc/hooks/products/useProductsLightweight.tssrc/lib/external-db/products-lightweight.tssrc/types/product-catalog.ts
| onError={(e) => { | ||
| (e.currentTarget as HTMLImageElement).src = '/placeholder.svg'; | ||
| }} | ||
| /> |
There was a problem hiding this comment.
Bug crítico: loop infinito se placeholder falhar.
O onError na linha 107 seta src para '/placeholder.svg', mas se o próprio placeholder falhar (arquivo faltando, erro de rede, CORS), dispara onError de novo → loop infinito. Browser pode travar ou consumir memória descontroladamente.
Solução: adicionar guard para evitar trocar novamente se já estiver no placeholder.
🛡️ Fix proposto para prevenir loop
onError={(e) => {
- (e.currentTarget as HTMLImageElement).src = '/placeholder.svg';
+ const img = e.currentTarget as HTMLImageElement;
+ if (img.src !== '/placeholder.svg' && !img.src.endsWith('/placeholder.svg')) {
+ img.src = '/placeholder.svg';
+ }
}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onError={(e) => { | |
| (e.currentTarget as HTMLImageElement).src = '/placeholder.svg'; | |
| }} | |
| /> | |
| onError={(e) => { | |
| const img = e.currentTarget as HTMLImageElement; | |
| if (img.src !== '/placeholder.svg' && !img.src.endsWith('/placeholder.svg')) { | |
| img.src = '/placeholder.svg'; | |
| } | |
| }} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/catalog/ProductCardImage.tsx` around lines 106 - 109, The
onError handler in ProductCardImage.tsx currently always sets (e.currentTarget
as HTMLImageElement).src = '/placeholder.svg' which can cause an infinite loop
if the placeholder fails; modify the onError handler to first check the current
image src or a flag (e.g., img.dataset['errored'] or compare
e.currentTarget.src) and only set src to '/placeholder.svg' if it isn’t already
the placeholder (and mark the image as errored to prevent repeated retries),
updating the onError in the ProductCardImage component accordingly.
| async function loadCategoriesMap(): Promise<ReadonlyMap<string, string>> { | ||
| if (!categoriesMapPromise) { | ||
| categoriesMapPromise = fetchPromobrindCategories() | ||
| .then((categories) => new Map(categories.map((c) => [String(c.id), c.name])) as ReadonlyMap<string, string>) | ||
| .catch(() => { | ||
| categoriesMapPromise = null; // allow retry on next request | ||
| return new Map() as ReadonlyMap<string, string>; | ||
| }); | ||
| try { | ||
| const categories = await fetchPromobrindCategories(); | ||
| return new Map(categories.map((c) => [String(c.id), c.name])); | ||
| } catch { | ||
| return new Map(); | ||
| } | ||
| return categoriesMapPromise; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Procurar a definição de fetchPromobrindCategories e checar caching interno
ast-grep --pattern 'export async function fetchPromobrindCategories($$$) { $$$ }'
ast-grep --pattern 'export const fetchPromobrindCategories = $_'
# Contexto adicional: procurar caches/memo no arquivo de categorias
fd -i 'products-detail' --exec rg -n -C3 'cache|memo|singleton|staleTime|let .*Promise|Map\(' {}Repository: adm01-debug/promo-gifts-v4
Length of output: 2729
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Encontrar uso/definição de fetchPromobrindCategories no repo
rg -n "fetchPromobrindCategories" .
# 2) Mostrar o contexto do arquivo alvo (linhas 80-200)
sed -n '80,200p' src/hooks/products/useProductsLightweight.ts
# 3) Localizar o arquivo que exporta fetchPromobrindCategories (se houver) e procurar cache/memo
# (procura padrões típicos no arquivo que tiver o símbolo; fallback: busca por palavra-chave no repo)
FILES="$(rg -l "fetchPromobrindCategories" . || true)"
for f in $FILES; do
echo "---- $f ----"
sed -n '1,120p' "$f" | cat
rg -n "cache|memo|singleton|staleTime|let .*Promise|Map\(" "$f" || true
doneRepository: adm01-debug/promo-gifts-v4
Length of output: 25046
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Mostrar o corpo de fetchPromobrindCategories e checar caching
sed -n '480,620p' src/lib/external-db/products-detail.ts
# Buscar no arquivo qualquer uso de immutableCache relacionado a categorias
rg -n "fetchPromobrindCategories|getFreshFromCacheSafe|putInCacheSafe|getCachedByIds|immutableCache|categories" src/lib/external-db/products-detail.tsRepository: adm01-debug/promo-gifts-v4
Length of output: 4672
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Ver como fetchCatalogPage é usado no useInfiniteQuery (freq. por página)
sed -n '120,340p' src/hooks/products/useProductsLightweight.ts
# 2) Ver como funciona immutableCache (se há leitura/early return quando categorias estão no cache)
ls src/lib/external-db | sed -n '1,200p'
fd -i 'immutableCache' src/lib/external-db -a
# (fallback caso o nome não esteja exatamente)
FILES="$(rg -l "putInCacheSafe|getFreshFromCacheSafe|getCachedByIds" src/lib/external-db || true)"
for f in $FILES; do
echo "---- $f ----"
rg -n "function putInCacheSafe|putInCacheSafe\\(|getFreshFromCacheSafe|getCachedByIds\\(" "$f" || true
done
# Abrir o arquivo que contém as defs (preferir o provável)
if [ -f src/lib/external-db/immutableCache.ts ]; then
sed -n '1,260p' src/lib/external-db/immutableCache.ts
fiRepository: adm01-debug/promo-gifts-v4
Length of output: 12321
Memoize categorias no useProductsCatalog (evita N chamadas de rede ao scroll)
loadCategoriesMap chama fetchPromobrindCategories e monta um novo Map; como fetchCatalogPage executa isso dentro do queryFn, cada página carregada no useInfiniteQuery dispara novamente a busca das categorias.
Além disso, fetchPromobrindCategories não faz early-return do immutableCache: ele sempre roda dbInvoke em categories e só depois preenche putInCacheSafe('categories', ...), então o cache atual não evita essas chamadas.
Sugerido: memoizar a promessa/lista de categorias (ex.: ref escopo do hook ou cache de módulo com TTL) e reutilizar o categoriesById nas páginas; ou reaproveitar useCategories/React Query (mesmo staleTime: 30 min) e passar o mapa para mapLightweightToProduct.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/products/useProductsLightweight.ts` around lines 112 - 119,
loadCategoriesMap currently calls fetchPromobrindCategories on every queryFn run
(triggered by fetchCatalogPage in useProductsCatalog/useInfiniteQuery) and does
not short‑circuit when immutableCache exists, causing repeated network/dbInvoke;
memoize the categories promise or result at hook/module scope (or reuse the
existing useCategories React Query hook with a long staleTime) and change
loadCategoriesMap to return the cached Promise/Map (or early‑return when
immutableCache present after checking putInCacheSafe) so mapLightweightToProduct
and fetchCatalogPage reuse the single categoriesById instance instead of
refetching on each page load.
Hover Image no Card do Catálogo
Ao passar o mouse sobre um produto, a imagem muda para a foto com todas as cores disponíveis. Ao tirar o mouse, volta para a imagem principal.
Banco de dados — já aplicado em produção ✅
Migration executada diretamente no Supabase
doufsxqlfjyuvxuezpln:is_primary:mainvira primáriagallery → setADD COLUMN products.set_image_urlset_image_urltrg_sync_set_image_urlCobertura: 4.086 / 6.086 produtos (67,1%)
setdisponívelFrontend — 4 arquivos neste PR
src/lib/external-db/products-lightweight.ts+set_image_urlnoLightweightProductePRODUCT_SELECT_LIGHTWEIGHTsrc/types/product-catalog.ts+set_image_url?: string | nullna interfaceProductsrc/hooks/products/useProductsLightweight.ts+set_image_urlno SELECT + mapeamento emmapLightweightToProductsrc/components/catalog/ProductCardImage.tsxUso do componente (passo final — não neste PR)
Substituir o
<img>atual do ProductCard:Performance
set_image_urlvem no SELECT inicialgroup/group-hover— zero JS no hoverRollback
Summary by cubic
Adds hover image to catalog product cards with a smooth crossfade from the main image to the “set” image (all colors). Introduces
set_image_urlin the catalog data so the effect needs no extra requests and is CSS-only.ProductCardImagewith CSS crossfade on hover.set_image_urladded toLightweightProduct,Product, andPRODUCT_SELECT_LIGHTWEIGHT, and mapped inmapLightweightToProduct./publicvariant is auto-applied when missing.Written for commit 332e7e8. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
New Features
Melhorias