optimizations
This commit is contained in:
@@ -4,6 +4,30 @@ import React, {
|
||||
import ArtworkGallery from '../artwork/ArtworkGallery';
|
||||
import './MasonryGallery.css';
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
}
|
||||
|
||||
async function sendDiscoveryEvent(endpoint, payload) {
|
||||
if (!endpoint) return;
|
||||
|
||||
try {
|
||||
await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
keepalive: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': getCsrfToken(),
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch {
|
||||
// Discovery telemetry should never block the gallery UX.
|
||||
}
|
||||
}
|
||||
|
||||
// ── Masonry helpers ────────────────────────────────────────────────────────
|
||||
const ROW_SIZE = 8;
|
||||
const ROW_GAP = 16;
|
||||
@@ -89,6 +113,7 @@ async function fetchPageData(url) {
|
||||
nextCursor,
|
||||
nextPageUrl,
|
||||
hasMore,
|
||||
meta: json.meta ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,6 +131,7 @@ async function fetchPageData(url) {
|
||||
nextCursor: el.dataset.nextCursor || null,
|
||||
nextPageUrl: el.dataset.nextPageUrl || null,
|
||||
hasMore: null,
|
||||
meta: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,6 +219,12 @@ function getMasonryCardProps(art, idx) {
|
||||
decoding: idx < 8 ? 'sync' : 'async',
|
||||
fetchPriority: idx === 0 ? 'high' : undefined,
|
||||
imageClassName: 'nova-card-main-image absolute inset-0 h-full w-full object-cover group-hover:scale-[1.03]',
|
||||
metricBadge: art.recommendation_reason
|
||||
? {
|
||||
label: art.recommendation_reason,
|
||||
className: 'bg-sky-500/14 text-sky-100 ring-sky-300/30 max-w-[15rem] truncate',
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,15 +254,20 @@ function MasonryGallery({
|
||||
rankApiEndpoint = null,
|
||||
rankType = null,
|
||||
gridClassName = null,
|
||||
discoveryEndpoint = null,
|
||||
algoVersion: initialAlgoVersion = null,
|
||||
}) {
|
||||
const [artworks, setArtworks] = useState(initialArtworks);
|
||||
const [nextCursor, setNextCursor] = useState(initialNextCursor);
|
||||
const [nextPageUrl, setNextPageUrl] = useState(initialNextPageUrl);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [done, setDone] = useState(!initialNextCursor && !initialNextPageUrl);
|
||||
const [algoVersion, setAlgoVersion] = useState(initialAlgoVersion);
|
||||
|
||||
const gridRef = useRef(null);
|
||||
const triggerRef = useRef(null);
|
||||
const viewedArtworkIdsRef = useRef(new Set());
|
||||
const clickedArtworkIdsRef = useRef(new Set());
|
||||
|
||||
// ── Ranking API fallback ───────────────────────────────────────────────
|
||||
// When the server-side render provides no initial artworks (e.g. cache miss
|
||||
@@ -298,9 +335,13 @@ function MasonryGallery({
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { artworks: newItems, nextCursor: nc, nextPageUrl: np, hasMore } =
|
||||
const { artworks: newItems, nextCursor: nc, nextPageUrl: np, hasMore, meta } =
|
||||
await fetchPageData(fetchUrl);
|
||||
|
||||
if (meta?.algo_version) {
|
||||
setAlgoVersion(meta.algo_version);
|
||||
}
|
||||
|
||||
if (!newItems.length) {
|
||||
setDone(true);
|
||||
} else {
|
||||
@@ -334,6 +375,95 @@ function MasonryGallery({
|
||||
return () => io.disconnect();
|
||||
}, [done, fetchNext]);
|
||||
|
||||
useEffect(() => {
|
||||
if (galleryType !== 'for-you' || !discoveryEndpoint) return undefined;
|
||||
|
||||
const grid = gridRef.current;
|
||||
if (!grid || !(window.IntersectionObserver)) return undefined;
|
||||
|
||||
const artworkIndex = new Map(artworks.map((art, index) => [String(art.id), { art, index }]));
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting || entry.intersectionRatio < 0.65) {
|
||||
return;
|
||||
}
|
||||
|
||||
const card = entry.target.closest('[data-art-id]');
|
||||
const artworkId = card?.getAttribute('data-art-id');
|
||||
if (!artworkId || viewedArtworkIdsRef.current.has(artworkId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candidate = artworkIndex.get(artworkId);
|
||||
if (!candidate?.art?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewedArtworkIdsRef.current.add(artworkId);
|
||||
observer.unobserve(entry.target);
|
||||
sendDiscoveryEvent(discoveryEndpoint, {
|
||||
event_type: 'view',
|
||||
artwork_id: Number(candidate.art.id),
|
||||
algo_version: candidate.art.recommendation_algo_version || algoVersion || undefined,
|
||||
meta: {
|
||||
gallery_type: galleryType,
|
||||
position: candidate.index + 1,
|
||||
source: candidate.art.recommendation_source || null,
|
||||
reason: candidate.art.recommendation_reason || null,
|
||||
score: candidate.art.recommendation_score ?? null,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
{ threshold: [0.65] },
|
||||
);
|
||||
|
||||
const cards = grid.querySelectorAll('[data-art-id]');
|
||||
cards.forEach((card) => observer.observe(card));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [algoVersion, artworks, discoveryEndpoint, galleryType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (galleryType !== 'for-you' || !discoveryEndpoint) return undefined;
|
||||
|
||||
const grid = gridRef.current;
|
||||
if (!grid) return undefined;
|
||||
|
||||
const handleClick = (event) => {
|
||||
const card = event.target.closest('[data-art-id]');
|
||||
if (!card) return;
|
||||
|
||||
const artworkId = card.getAttribute('data-art-id');
|
||||
if (!artworkId || clickedArtworkIdsRef.current.has(artworkId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const artwork = artworks.find((item) => String(item.id) === artworkId);
|
||||
if (!artwork?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
clickedArtworkIdsRef.current.add(artworkId);
|
||||
sendDiscoveryEvent(discoveryEndpoint, {
|
||||
event_type: 'click',
|
||||
artwork_id: Number(artwork.id),
|
||||
algo_version: artwork.recommendation_algo_version || algoVersion || undefined,
|
||||
meta: {
|
||||
gallery_type: galleryType,
|
||||
source: artwork.recommendation_source || null,
|
||||
reason: artwork.recommendation_reason || null,
|
||||
score: artwork.recommendation_score ?? null,
|
||||
target_url: artwork.url || null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
grid.addEventListener('click', handleClick, true);
|
||||
return () => grid.removeEventListener('click', handleClick, true);
|
||||
}, [algoVersion, artworks, discoveryEndpoint, galleryType]);
|
||||
|
||||
// Gallery V2 spec §7: 5 col desktop / 3 tablet / 2 mobile for all gallery pages.
|
||||
// Discover feeds (home/discover page) retain the same 5-col layout.
|
||||
const gridClass = gridClassName || 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6';
|
||||
|
||||
Reference in New Issue
Block a user