Skip to content

feat(catalog): hover image — main → set crossfade no card de produto#598

Merged
adm01-debug merged 1 commit into
mainfrom
feat/catalog-hover-image
Jun 2, 2026
Merged

feat(catalog): hover image — main → set crossfade no card de produto#598
adm01-debug merged 1 commit into
mainfrom
feat/catalog-hover-image

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented Jun 2, 2026

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:

Passo Ação Resultado
1 Fix SPOT is_primary: main vira primária 1.174 produtos corrigidos
2 XBZ: reclassifica d1 gallery → set 2.560 produtos com hover
3 ADD COLUMN products.set_image_url Nova coluna
4 Backfill set_image_url 4.086 produtos populados
5 Trigger trg_sync_set_image_url Sync automático
6 Suffix mapping XBZ d1 Futuros imports classificados

Cobertura: 4.086 / 6.086 produtos (67,1%)

Fornecedor Hover ativo
SPOT / Stricker ✅ ~1.163 produtos
XBZ Brindes ✅ ~2.560 produtos
Asia Import ✅ ~363 produtos
Só Marcas ❌ sem set disponível

Frontend — 4 arquivos neste PR

Arquivo Mudança
src/lib/external-db/products-lightweight.ts +set_image_url no LightweightProduct e PRODUCT_SELECT_LIGHTWEIGHT
src/types/product-catalog.ts +set_image_url?: string | null na interface Product
src/hooks/products/useProductsLightweight.ts +set_image_url no SELECT + mapeamento em mapLightweightToProduct
src/components/catalog/ProductCardImage.tsx NOVO — componente de crossfade CSS

Uso do componente (passo final — não neste PR)

Substituir o <img> atual do ProductCard:

import { ProductCardImage } from '@/components/catalog/ProductCardImage';

// Dentro do JSX do card:
<ProductCardImage
  mainUrl={product.image_url}
  setUrl={product.set_image_url}
  alt={product.name}
/>

Performance

  • ✅ Zero queries extras no hover — set_image_url vem no SELECT inicial
  • ✅ Crossfade 100% CSS via Tailwind group/group-hover — zero JS no hover
  • ✅ Ambas as imagens pré-carregadas pelo browser no render do card

Rollback

-- Reverter DB se necessário:
ALTER TABLE products DROP COLUMN set_image_url;
DROP TRIGGER trg_sync_set_image_url ON product_images;
UPDATE product_images SET image_type='gallery'
  WHERE source_supplier='XBZ' AND image_type='set' AND cloudflare_image_id LIKE '%-d1';

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_url in the catalog data so the effect needs no extra requests and is CSS-only.

  • New Features
    • New ProductCardImage with CSS crossfade on hover.
    • set_image_url added to LightweightProduct, Product, and PRODUCT_SELECT_LIGHTWEIGHT, and mapped in mapLightweightToProduct.
    • Both images are rendered and lazy-loaded; Cloudflare /public variant is auto-applied when missing.

Written for commit 332e7e8. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Cards de produtos agora exibem imagens alternativas ao passar o mouse, permitindo visualizar as variações de cores disponíveis de forma interativa.
  • Melhorias

    • Otimizações no carregamento e exibição do catálogo de produtos para melhor desempenho da aplicação.

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
Copilot AI review requested due to automatic review settings June 2, 2026 10:30
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
we-dream-big Ready Ready Preview, Comment Jun 2, 2026 10:31am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Walkthrough

Adiçã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.

Changes

Imagens de produto com set_image_url e simplificação de catálogo

Layer / File(s) Summary
Contratos de tipos para set_image_url
src/types/product-catalog.ts, src/lib/external-db/products-lightweight.ts
Product e LightweightProduct ganham campo set_image_url?: string | null para imagem alternativa; ProductFilters perde sortBy; PRODUCT_SELECT_LIGHTWEIGHT passa a selecionar set_image_url da base de dados.
Mapeamento de set_image_url no hook
src/hooks/products/useProductsLightweight.ts
mapLightweightToProduct extrai p.set_image_url e o inclui no objeto de saída para alimentar o componente visual.
Simplificação de catálogo (remove sortBy, melhora loadCategoriesMap)
src/hooks/products/useProductsLightweight.ts
fetchCatalogPage deixa de aceitar sortBy (mantém orderBy fixo por name asc); loadCategoriesMap remove singleton module-level e faz fallback direto em catch; useProductsCatalog atualiza queryKey e queryFn para refletir remoção de ordenação.
Componente ProductCardImage com crossfade
src/components/catalog/ProductCardImage.tsx
Novo componente que renderiza imagem principal com opcionalmente uma segunda imagem (set) que aparece no hover via Tailwind group-hover, incluindo conversão Cloudflare Images (toCfUrl) e fallback para /placeholder.svg em caso de erro.
Limpeza de comentários em tipos e queries
src/types/product-catalog.ts, src/lib/external-db/products-lightweight.ts
Remoção de documentação desnecessária em campos de precificação e comentários de estratégia de paginação; campos mantêm as mesmas assinaturas.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • adm01-debug/promo-gifts-v4#40: Ambas as PRs modificam mapLightweightToProduct em useProductsLightweight.ts; a PR #40 adiciona resolução de nomes de categorias via categoriesById, enquanto esta adiciona set_image_url e remove sortBy.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título descreve de forma clara e específica a mudança principal: implementar efeito crossfade na imagem do card ao passar o mouse (main → set), alinhado com o objetivo central do PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/catalog-hover-image

Comment @coderabbitai help to get the list of available commands and usage tips.

@supabase
Copy link
Copy Markdown

supabase Bot commented Jun 2, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_url aos tipos do catálogo e ao payload “lightweight” (SELECT + interface).
  • Inclui set_image_url no hook do catálogo (useProductsCatalog) e mapeia para Product.
  • Cria src/components/catalog/ProductCardImage.tsx para 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.

Comment on lines 175 to 183
export interface ProductFilters {
category?: string;
categoryId?: string | number;
search?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
limit?: number;
sortBy?: string;
}
Comment on lines 231 to 235
export function useProductsCatalog(filters?: {
search?: string;
categories?: string[];
suppliers?: string[];
sortBy?: string;
}) {
Comment on lines 112 to 119
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;
}
Comment on lines +42 to +51
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;
}
Comment on lines +113 to +117
<img
src={setSrc}
alt={`${alt} — todas as cores`}
loading="lazy"
decoding="async"
Comment on lines +123 to +126
onError={(e) => {
// Se imagem set falhar, esconde para evitar broken image
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between de41d16 and 332e7e8.

📒 Files selected for processing (4)
  • src/components/catalog/ProductCardImage.tsx
  • src/hooks/products/useProductsLightweight.ts
  • src/lib/external-db/products-lightweight.ts
  • src/types/product-catalog.ts

Comment on lines +106 to +109
onError={(e) => {
(e.currentTarget as HTMLImageElement).src = '/placeholder.svg';
}}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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.

Suggested change
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.

Comment on lines 112 to 119
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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
done

Repository: 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.ts

Repository: 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
fi

Repository: 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.

@adm01-debug adm01-debug merged commit 3957f11 into main Jun 2, 2026
34 of 43 checks passed
@adm01-debug adm01-debug deleted the feat/catalog-hover-image branch June 2, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants