Skip to content
Open
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
55 changes: 49 additions & 6 deletions src/components/MarketplaceGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo, useMemo } from 'react'
import type { MarketplaceCardProps } from './MarketplaceCard'
import { MarketplaceCard } from './MarketplaceCard'
import { EmptyState } from '@/components/ui/EmptyState'
Expand All @@ -8,16 +9,51 @@ export interface MarketplaceGridProps {
isCompareFull?: boolean
onCompareToggle?: (listing: MarketplaceCardProps) => void
onView?: (id: string) => void
/** Optional comparator applied before rendering. Stabilize with useCallback. */
sortFn?: (a: MarketplaceCardProps, b: MarketplaceCardProps) => number
/** Optional predicate applied before rendering. Stabilize with useCallback. */
filterFn?: (item: MarketplaceCardProps) => boolean
}

export function MarketplaceGrid({
// VIRTUALIZATION THRESHOLD — engage CSS content-visibility windowing only
// when the list is large enough to justify the overhead.
const VIRTUALIZE_THRESHOLD = 50

/**
* MarketplaceGrid
*
* Performance notes:
* - `filterFn` / `sortFn` props are applied via `useMemo` so the derived
* list is only recomputed when `items`, `filterFn`, or `sortFn` change.
* - `MarketplaceCard` is already wrapped in `React.memo`, so only cards
* with changed props are re-rendered during filter/sort updates.
* - For lists larger than VIRTUALIZE_THRESHOLD the grid applies CSS
* `content-visibility: auto` per card — a zero-dependency windowing
* approach that lets the browser skip layout/paint for off-screen items
* while preserving DOM presence for accessibility and SSR compatibility.
*/
export const MarketplaceGrid = memo(function MarketplaceGrid({
items,
isComparePinned,
isCompareFull = false,
onCompareToggle,
onView,
sortFn,
filterFn,
}: MarketplaceGridProps) {
if (!items || items.length === 0) {
// Memoize derived list — only recomputes when items / predicates change.
const displayedItems = useMemo(() => {
let result = items
if (filterFn) {
result = result.filter(filterFn)
}
if (sortFn) {
result = [...result].sort(sortFn)
}
return result
}, [items, filterFn, sortFn])

if (!displayedItems || displayedItems.length === 0) {
return (
<section className="mt-10" aria-label="Marketplace listings">
<EmptyState
Expand All @@ -29,13 +65,21 @@ export function MarketplaceGrid({
)
}

const isLargeList = displayedItems.length > VIRTUALIZE_THRESHOLD

return (
<section className="mt-6" aria-label="Marketplace listings">
<ul className="list-none p-0 m-0 grid grid-cols-3 gap-6 max-[1024px]:grid-cols-2 max-[720px]:grid-cols-1">
{items.map((item) => {
{displayedItems.map((item) => {
const compareSelected = isComparePinned?.(item.id) ?? false
return (
<li key={item.id} className="min-h-[280px]">
<li
key={item.id}
className="min-h-[280px]"
// content-visibility: auto defers rendering of off-screen list
// items; contain-intrinsic-size prevents scroll-bar jank.
style={isLargeList ? { contentVisibility: 'auto', containIntrinsicSize: '0 320px' } : undefined}
>
<MarketplaceCard
{...item}
compareSelected={compareSelected}
Expand All @@ -51,5 +95,4 @@ export function MarketplaceGrid({
</ul>
</section>
)
}

})
79 changes: 65 additions & 14 deletions src/components/MyCommitmentsGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React from 'react';
import React, { memo, useMemo } from 'react';
import MyCommitmentCard from './MyCommitmentCard';
import { Commitment } from '@/types/commitment';
import { EmptyState } from '@/components/ui/EmptyState';
Expand All @@ -11,33 +11,82 @@ interface MyCommitmentsGridProps {
onAttestations?: (id: string) => void;
onEarlyExit?: (id: string) => void;
onListForSale?: (id: string) => void;
/** Optional comparator to sort commitments before rendering.
* Memoized internally so callers should stabilize the reference. */
sortFn?: (a: Commitment, b: Commitment) => number;
/** Optional predicate to filter commitments before rendering.
* Memoized internally so callers should stabilize the reference. */
filterFn?: (c: Commitment) => boolean;
}

const MyCommitmentsGrid: React.FC<MyCommitmentsGridProps> = ({
// VIRTUALIZATION THRESHOLD — only engage virtual windowing when the list
// exceeds this length to avoid overhead for typical (small) datasets.
const VIRTUALIZE_THRESHOLD = 50;

/**
* MyCommitmentsGrid
*
* Performance notes:
* - `sortFn` / `filterFn` are applied via `useMemo` so the derived list
* is only recomputed when `commitments`, `sortFn`, or `filterFn` change.
* - Each `MyCommitmentCard` is already wrapped in `React.memo`, so cards
* whose props are unchanged are skipped during reconciliation.
* - When the list exceeds VIRTUALIZE_THRESHOLD items the grid switches to a
* CSS `content-visibility: auto` approach (no extra runtime dependency).
* This lets the browser skip layout/paint for off-screen rows while
* keeping the DOM present for accessibility and SSR. A full windowing
* library (react-window, TanStack Virtual) would give larger gains but
* requires a new dependency; this lighter approach is intentionally
* dependency-conscious as the issue requests.
*/
const MyCommitmentsGrid: React.FC<MyCommitmentsGridProps> = memo(({
commitments,
onDetails,
onAttestations,
onEarlyExit,
onListForSale,
sortFn,
filterFn,
}) => {
// Memoize the derived list so filter+sort only run when inputs change.
const displayedCommitments = useMemo(() => {
let result = commitments;
if (filterFn) {
result = result.filter(filterFn);
}
if (sortFn) {
result = [...result].sort(sortFn);
}
return result;
}, [commitments, filterFn, sortFn]);

const isLargeList = displayedCommitments.length > VIRTUALIZE_THRESHOLD;

return (
<div className="flex flex-col gap-4">
<div className="text-[14px] text-[#94A3B8]">
<span className="text-[16px] font-semibold text-white">{commitments.length}</span>{' '}
<span className="text-[16px] font-semibold text-white">{displayedCommitments.length}</span>{' '}
commitments found
</div>
{commitments.length > 0 ? (

{displayedCommitments.length > 0 ? (
<div className="grid grid-cols-3 gap-6 max-[1200px]:grid-cols-2 max-[768px]:grid-cols-1">
{commitments.map((commitment) => (
<MyCommitmentCard
{displayedCommitments.map((commitment) => (
<div
key={commitment.id}
commitment={commitment}
onDetails={onDetails}
onAttestations={onAttestations}
onEarlyExit={onEarlyExit}
onListForSale={onListForSale}
/>
// content-visibility: auto tells the browser to skip rendering
// work for off-screen items; contain-intrinsic-size prevents
// layout shift as items scroll into view.
style={isLargeList ? { contentVisibility: 'auto', containIntrinsicSize: '0 380px' } : undefined}
>
<MyCommitmentCard
commitment={commitment}
onDetails={onDetails}
onAttestations={onAttestations}
onEarlyExit={onEarlyExit}
onListForSale={onListForSale}
/>
</div>
))}
</div>
) : (
Expand All @@ -49,6 +98,8 @@ const MyCommitmentsGrid: React.FC<MyCommitmentsGridProps> = ({
)}
</div>
);
};
});

MyCommitmentsGrid.displayName = 'MyCommitmentsGrid';

export default MyCommitmentsGrid;
Loading