Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<link rel="dns-prefetch" href="https://pqpdolkaeqlyzpdpbizo.supabase.co" />
<link rel="preconnect" href="https://pqpdolkaeqlyzpdpbizo.supabase.co" crossorigin />
<link rel="dns-prefetch" href="https://imagedelivery.net" />
<link rel="dns-prefetch" href="https://doufsxqlfjyuvxuezpln.supabase.co" />

<!-- Load Google Fonts — non-render-blocking via media swap -->
<!-- Apenas Outfit (display) e Plus Jakarta Sans (sans). Inter foi removido
Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const App = () => {
<ThemeInitializer />
<AccessibilityProvider>
<AriaLiveProvider>
<TooltipProvider skipDelayDuration={300}>
<TooltipProvider delayDuration={400} skipDelayDuration={200}>
{/*
* Keep v7_startTransition disabled: under concurrent root work it can
* update history before the matching route render commits.
Expand Down
58 changes: 23 additions & 35 deletions src/components/products/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useState, useRef, useEffect, memo, forwardRef, useCallback } from 'reac
import { GenderBadge } from './GenderBadge';
import { Building2, Package } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { getCdnUrl, getSrcSet } from '@/utils/image-utils';
import { cn } from '@/lib/utils';
import { useProductBounds } from '@/hooks/products/useProductBounds';
Expand Down Expand Up @@ -39,6 +38,23 @@ import { PriceFreshnessBadge } from './PriceFreshnessBadge';
import { feedback } from '@/lib/feedback';
import { telemetryService } from '@/services/telemetryService';

const priceFormatter = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
const formatPrice = (price: number) => priceFormatter.format(price);

const STOCK_STATUS_COLOR: Record<string, string> = {
'in-stock': 'in-stock',
'low-stock': 'low-stock',
'out-of-stock': 'out-of-stock',
};
const getStockStatusColor = (status: string) => STOCK_STATUS_COLOR[status] ?? 'in-stock';

const STOCK_STATUS_LABEL: Record<string, string> = {
'in-stock': 'Em estoque',
'low-stock': 'Estoque baixo',
'out-of-stock': 'Sem estoque',
};
const getStockStatusLabel = (status: string) => STOCK_STATUS_LABEL[status] ?? 'Em estoque';

export interface ProductCardProps {
product: Product;
onClick?: () => void;
Expand Down Expand Up @@ -83,7 +99,6 @@ export const ProductCard = memo(
ref,
) {
const navigate = useNavigate();
const _queryClient = useQueryClient();
const { prefetchProduct } = usePrefetchProduct();
// Categoria-FOLHA (mais específica) resolvida em lote pelo ProductLeafCategoryProvider.
// Quando disponível, sobrepõe a categoria "rasa" (raiz/intermediária) no badge.
Expand Down Expand Up @@ -125,8 +140,8 @@ export const ProductCard = memo(
const [variantPickerOpen, setVariantPickerOpen] = useState(false);
const [variantPickerMode, setVariantPickerMode] = useState<VariantActionMode>('favorite');

const favStore = useFavoritesStore();
const compStore = useComparisonStore();
const addFavorite = useFavoritesStore((s) => s.addFavorite);
const addToCompare = useComparisonStore((s) => s.addToCompare);

const handleStatusClick = useCallback(
(type: string, _value?: string | number) => {
Expand Down Expand Up @@ -166,13 +181,13 @@ export const ProductCard = memo(
: undefined;

if (variantPickerMode === 'favorite') {
favStore.addFavorite(product.id, variantInfo);
addFavorite(product.id, variantInfo);
feedback.light();
toast.success(
`"${product.name}" favoritado${variant?.color_name ? ` — ${variant.color_name}` : ''}`,
);
} else if (variantPickerMode === 'compare') {
const result = compStore.addToCompare(product.id, variantInfo);
const result = addToCompare(product.id, variantInfo);
if (!result) {
feedback.error();
showErrorToast({ title: 'Limite de 4 produtos para comparação atingido' });
Expand Down Expand Up @@ -211,7 +226,7 @@ export const ProductCard = memo(
setShareDialogOpen(true);
}
},
[variantPickerMode, product, favStore, compStore, navigate],
[variantPickerMode, product, addFavorite, addToCompare, navigate],
);

const markBusy = () => {
Expand Down Expand Up @@ -257,33 +272,6 @@ export const ProductCard = memo(
}
};

const formatPrice = (price: number) =>
new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(price);

const getStockStatusColor = (status: string) => {
switch (status) {
case 'in-stock':
return 'in-stock';
case 'low-stock':
return 'low-stock';
case 'out-of-stock':
return 'out-of-stock';
default:
return 'in-stock';
}
};
const getStockStatusLabel = (status: string) => {
switch (status) {
case 'in-stock':
return 'Em estoque';
case 'low-stock':
return 'Estoque baixo';
case 'out-of-stock':
return 'Sem estoque';
default:
return 'Em estoque';
}
};

// Multi-variant carousel
const allMatchingVariants = resolveAllMatchingColors(product.colors, activeColorFilter);
Expand Down Expand Up @@ -451,7 +439,7 @@ export const ProductCard = memo(
<div
className={cn(
'relative space-y-2.5 p-3 transition-all duration-500 sm:space-y-4 sm:p-5',
isHovered ? 'translate-y-[-2px] bg-background/95 backdrop-blur-md' : 'bg-background',
isHovered ? 'translate-y-[-2px] bg-background' : 'bg-background',
)}
style={{ zIndex: 10 }}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/products/ProductTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const stockColor = (status: string) => {
};

const CONTAINER_CLASS =
'h-[calc(100vh-200px)] min-h-[550px] overflow-y-auto rounded-xl border border-border/40 bg-gradient-to-b from-background/80 to-background/40 backdrop-blur-sm scrollbar-products shadow-inner';
'h-[calc(100vh-200px)] min-h-[550px] overflow-y-auto rounded-xl border border-border/40 bg-background scrollbar-products shadow-sm';

function SortHeader({
label,
Expand Down
35 changes: 15 additions & 20 deletions src/components/products/VirtualizedProductGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useCallback, useState, useEffect } from 'react';
import { useRef, useCallback, useState, useEffect, useMemo } from 'react';
import { cn } from '@/lib/utils';
import { useVirtualizer } from '@tanstack/react-virtual';
import { motion, AnimatePresence } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { Loader2, ArrowUp } from 'lucide-react';
import { ProductCard } from './ProductCard';
import { ProductListItem } from './ProductListItem';
Expand Down Expand Up @@ -80,14 +80,13 @@ export function VirtualizedProductGrid({
// In list mode, always 1 column; in grid mode use columns prop
const effectiveColumns = viewMode === 'list' ? 1 : columns;

// Column gap varies by density, row gap is always consistent (16px = gap-y-4)
const getColumnGapPx = () => {
// Column gap varies by density, row gap is always consistent
const colGapPx = useMemo(() => {
if (effectiveColumns >= 8) return 16;
if (effectiveColumns >= 6) return 24;
return 32;
};
const colGapPx = getColumnGapPx();
const rowGapPx = viewMode === 'list' ? 8 : 32; // list: compact 8px gap; grid: 32px
}, [effectiveColumns]);
const rowGapPx = viewMode === 'list' ? 8 : 32;

// Calculate rows based on columns
const rowCount = Math.ceil(products.length / effectiveColumns);
Expand All @@ -104,7 +103,7 @@ export function VirtualizedProductGrid({
count: hasMore ? rowCount + 1 : rowCount,
getScrollElement: () => parentRef.current,
estimateSize: () => estimatedRowHeight,
overscan: viewMode === 'list' ? 8 : 3,
overscan: viewMode === 'list' ? 10 : 5,
measureElement: (el) => el.getBoundingClientRect().height,
});

Expand Down Expand Up @@ -150,8 +149,8 @@ export function VirtualizedProductGrid({
return (
<div className="relative h-full">
<div
className="scrollbar-products h-[calc(100vh-200px)] min-h-[600px] overflow-y-auto overscroll-contain rounded-xl border border-border/40 bg-gradient-to-b from-background/80 to-background/40 shadow-inner backdrop-blur-sm"
style={{ contain: 'strict', WebkitOverflowScrolling: 'touch' }}
className="scrollbar-products h-[calc(100vh-200px)] min-h-[600px] overflow-y-auto overscroll-contain rounded-xl border border-border/40 bg-background shadow-sm"
style={{ contain: 'strict' }}
>
{showFilterBar && onSortChange && onOpenFilters && onClearFilters && onViewModeChange && (
<div className="sticky top-[calc(var(--header-h,56px)+var(--breadcrumb-h,0px))] z-20 mb-2 border-b border-border bg-background/95 px-4 py-2.5 backdrop-blur-md">
Expand Down Expand Up @@ -203,8 +202,8 @@ export function VirtualizedProductGrid({
<div
ref={parentRef}
data-testid="virtualized-product-grid"
className="scrollbar-products h-[calc(100vh-200px)] min-h-[600px] overflow-y-auto overscroll-contain rounded-xl border border-border/40 bg-gradient-to-b from-background/80 to-background/40 shadow-inner backdrop-blur-sm"
style={{ contain: 'strict', WebkitOverflowScrolling: 'touch' }}
className="scrollbar-products h-[calc(100vh-200px)] min-h-[600px] overflow-y-auto overscroll-contain rounded-xl border border-border/40 bg-background shadow-sm"
style={{ contain: 'strict' }}
>
{/* Barra de filtros sticky DENTRO do container de scroll */}
{showFilterBar && onSortChange && onOpenFilters && onClearFilters && onViewModeChange && (
Expand Down Expand Up @@ -290,7 +289,7 @@ export function VirtualizedProductGrid({
}),
}}
>
{rowProducts.map((product, colIndex) =>
{rowProducts.map((product) =>
viewMode === 'list' ? (
<ProductListItem
key={product.id}
Expand All @@ -305,18 +304,14 @@ export function VirtualizedProductGrid({
onStatusClick={onStatusClick}
/>
) : (
<motion.div
<div
key={product.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: colIndex * 0.05 }}
className={cn(
'relative transition-all duration-200',
'relative',
selectionMode &&
selectedIds?.has(product.id) &&
'rounded-2xl shadow-md ring-2 ring-primary/50',
)}
style={{ zIndex: 1 }}
>
{selectionMode && (
<button
Expand Down Expand Up @@ -356,7 +351,7 @@ export function VirtualizedProductGrid({
activeColorFilter={activeColorFilter}
onStatusClick={onStatusClick}
/>
</motion.div>
</div>
),
)}
</div>
Expand Down
16 changes: 11 additions & 5 deletions src/hooks/products/useProductsLightweight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,19 @@ interface CatalogPage {
totalEstimate: number | null;
}

// Module-level singleton: fetched once per session, shared across all catalog pages.
let categoriesMapPromise: Promise<ReadonlyMap<string, string>> | null = null;

async function loadCategoriesMap(): Promise<ReadonlyMap<string, string>> {
try {
const categories = await fetchPromobrindCategories();
return new Map(categories.map((c) => [String(c.id), c.name]));
} catch {
return new Map();
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>;
});
}
return categoriesMapPromise;
}

async function fetchCatalogPage(
Expand Down
8 changes: 8 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@
}
}

/* GPU-composited scroll containers — promotes to own layer to avoid repaints on scroll */
@layer utilities {
.scrollbar-products {
transform: translateZ(0);
will-change: scroll-position;
}
}
Comment on lines +86 to +92

/* Custom Tooltip Utilities (Ensures build retention) */
@layer utilities {
/* Tooltip: Standard (13px) */
Expand Down
2 changes: 2 additions & 0 deletions src/lib/query-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ const defaultQueryOptions: DefaultOptions = {
refetchOnWindowFocus: false,
refetchOnReconnect: true,
refetchOnMount: true,
// Use cached data while offline instead of erroring immediately
networkMode: 'offlineFirst',
},
mutations: {
retry: false,
Expand Down
Loading
Loading