Fix responsive layout with max items per row for each breakpoint
This commit is contained in:
@ -44,6 +44,7 @@ type Props = {
|
||||
maxRowItems?: number; // desktop
|
||||
maxRowItemsMobile?: number; // <640px
|
||||
gap?: number; // px
|
||||
debug?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@ -84,6 +85,7 @@ export default function JustifiedGallery({
|
||||
maxRowItems = 5,
|
||||
maxRowItemsMobile = 3,
|
||||
gap = 12,
|
||||
debug = false,
|
||||
className,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -125,7 +127,6 @@ export default function JustifiedGallery({
|
||||
|
||||
const isMobile = containerWidth < 640;
|
||||
const targetH = isMobile ? targetRowHeightMobile : targetRowHeight;
|
||||
const minRowHeight = Math.round(targetH * 0.8);
|
||||
const maxItems = (() => {
|
||||
if (containerWidth < 480) return Math.min(2, maxRowItemsMobile);
|
||||
if (containerWidth < 720) return Math.min(3, maxRowItems);
|
||||
@ -161,7 +162,10 @@ export default function JustifiedGallery({
|
||||
aspectSum = 0;
|
||||
};
|
||||
|
||||
for (const it of items) {
|
||||
const workingItems = items.slice();
|
||||
|
||||
for (let i = 0; i < workingItems.length; i += 1) {
|
||||
const it = workingItems[i];
|
||||
const a = aspectOf(it);
|
||||
|
||||
current.push({ item: it, aspect: a });
|
||||
@ -170,29 +174,51 @@ export default function JustifiedGallery({
|
||||
// Estimate the row width if we were to keep targetH
|
||||
const estimatedWidth = aspectSum * targetH + gap * (current.length - 1);
|
||||
|
||||
// If the computed height would be too small, split the row earlier.
|
||||
if (current.length > 1) {
|
||||
const gaps = gap * (current.length - 1);
|
||||
const widthWithoutGaps = Math.max(0, available - gaps);
|
||||
const computedH = widthWithoutGaps / aspectSum;
|
||||
if (computedH < minRowHeight) {
|
||||
const last = current.pop();
|
||||
if (last) {
|
||||
aspectSum -= last.aspect;
|
||||
flush();
|
||||
current = [last];
|
||||
aspectSum = last.aspect;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've filled the row (or reached max items) and have at least 2 tiles, flush.
|
||||
if (
|
||||
(estimatedWidth >= available || current.length >= maxItems) &&
|
||||
current.length > 1
|
||||
) {
|
||||
flush();
|
||||
const gaps = gap * (current.length - 1);
|
||||
const widthWithoutGaps = Math.max(0, available - gaps);
|
||||
const computedH = widthWithoutGaps / aspectSum;
|
||||
|
||||
// If the row would be shorter than maxRowHeight, reduce items and flush.
|
||||
if (computedH < maxRowHeight && current.length > 1) {
|
||||
const last = current.pop();
|
||||
if (last) {
|
||||
aspectSum -= last.aspect;
|
||||
}
|
||||
|
||||
const limit = widthWithoutGaps / maxRowHeight;
|
||||
let swapped = false;
|
||||
|
||||
for (let look = 1; look <= 2; look += 1) {
|
||||
const idx = i + look;
|
||||
if (idx >= workingItems.length) break;
|
||||
const candidate = workingItems[idx];
|
||||
const candidateAspect = aspectOf(candidate);
|
||||
|
||||
if (aspectSum + candidateAspect <= limit) {
|
||||
workingItems[idx] = last?.item ?? candidate;
|
||||
current.push({ item: candidate, aspect: candidateAspect });
|
||||
aspectSum += candidateAspect;
|
||||
swapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
flush();
|
||||
if (!swapped && last) {
|
||||
current = [last];
|
||||
aspectSum = last.aspect;
|
||||
} else {
|
||||
current = [];
|
||||
aspectSum = 0;
|
||||
}
|
||||
} else {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,27 +241,50 @@ export default function JustifiedGallery({
|
||||
return `${first}-${last}-${row.length}`;
|
||||
}, []);
|
||||
|
||||
const isSmallScreen = containerWidth < 640;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("mx-auto w-full max-w-6xl", className)}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={getRowKey(row)}
|
||||
className="flex justify-center"
|
||||
style={{ gap }}
|
||||
>
|
||||
{row.map((t) => (
|
||||
<GalleryTile
|
||||
key={t.item.id}
|
||||
tile={t}
|
||||
hrefBase={hrefBase}
|
||||
hrefFrom={hrefFrom}
|
||||
showCaption={showCaption}
|
||||
/>
|
||||
))}
|
||||
{rows.map((row, idx) => (
|
||||
<div key={getRowKey(row)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
row.length === 1 && (isSmallScreen || idx !== rows.length - 1)
|
||||
? "justify-center"
|
||||
: idx === rows.length - 1
|
||||
? "justify-start"
|
||||
: "justify-between",
|
||||
)}
|
||||
style={{ columnGap: gap }}
|
||||
>
|
||||
{row.map((t) => (
|
||||
<GalleryTile
|
||||
key={t.item.id}
|
||||
tile={t}
|
||||
hrefBase={hrefBase}
|
||||
hrefFrom={hrefFrom}
|
||||
showCaption={showCaption}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{debug ? (
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{`row ${idx + 1} | h=${Math.round(row[0]?.h ?? 0)} | w=${Math.round(
|
||||
row.reduce((sum, t) => sum + t.w, 0) + gap * (row.length - 1),
|
||||
)} | items=${row.length} | `}
|
||||
{row
|
||||
.map(
|
||||
(t) =>
|
||||
`${t.item.id}:${Math.round(t.w)}x${Math.round(t.h)} (src ${t.item.width}x${t.item.height})`,
|
||||
)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -90,17 +90,17 @@ export default function PortfolioGallery({
|
||||
dominantHex: it.dominantHex,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length === 0) return;
|
||||
// Debug: inspect dominantHex values coming from the server.
|
||||
console.log(
|
||||
"[PortfolioGallery] dominantHex sample",
|
||||
items.slice(0, 5).map((it) => ({
|
||||
id: it.id,
|
||||
dominantHex: it.dominantHex,
|
||||
}))
|
||||
);
|
||||
}, [items]);
|
||||
// useEffect(() => {
|
||||
// if (items.length === 0) return;
|
||||
// // Debug: inspect dominantHex values coming from the server.
|
||||
// console.log(
|
||||
// "[PortfolioGallery] dominantHex sample",
|
||||
// items.slice(0, 5).map((it) => ({
|
||||
// id: it.id,
|
||||
// dominantHex: it.dominantHex,
|
||||
// }))
|
||||
// );
|
||||
// }, [items]);
|
||||
|
||||
if (!loading && done && galleryItems.length === 0) {
|
||||
return (
|
||||
@ -116,6 +116,7 @@ export default function PortfolioGallery({
|
||||
items={galleryItems}
|
||||
hrefFrom="portfolio"
|
||||
showCaption={false}
|
||||
debug={false}
|
||||
targetRowHeight={160}
|
||||
targetRowHeightMobile={160}
|
||||
maxRowHeight={300}
|
||||
|
||||
Reference in New Issue
Block a user