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
|
maxRowItems?: number; // desktop
|
||||||
maxRowItemsMobile?: number; // <640px
|
maxRowItemsMobile?: number; // <640px
|
||||||
gap?: number; // px
|
gap?: number; // px
|
||||||
|
debug?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,6 +85,7 @@ export default function JustifiedGallery({
|
|||||||
maxRowItems = 5,
|
maxRowItems = 5,
|
||||||
maxRowItemsMobile = 3,
|
maxRowItemsMobile = 3,
|
||||||
gap = 12,
|
gap = 12,
|
||||||
|
debug = false,
|
||||||
className,
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -125,7 +127,6 @@ export default function JustifiedGallery({
|
|||||||
|
|
||||||
const isMobile = containerWidth < 640;
|
const isMobile = containerWidth < 640;
|
||||||
const targetH = isMobile ? targetRowHeightMobile : targetRowHeight;
|
const targetH = isMobile ? targetRowHeightMobile : targetRowHeight;
|
||||||
const minRowHeight = Math.round(targetH * 0.8);
|
|
||||||
const maxItems = (() => {
|
const maxItems = (() => {
|
||||||
if (containerWidth < 480) return Math.min(2, maxRowItemsMobile);
|
if (containerWidth < 480) return Math.min(2, maxRowItemsMobile);
|
||||||
if (containerWidth < 720) return Math.min(3, maxRowItems);
|
if (containerWidth < 720) return Math.min(3, maxRowItems);
|
||||||
@ -161,7 +162,10 @@ export default function JustifiedGallery({
|
|||||||
aspectSum = 0;
|
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);
|
const a = aspectOf(it);
|
||||||
|
|
||||||
current.push({ item: it, aspect: a });
|
current.push({ item: it, aspect: a });
|
||||||
@ -170,29 +174,51 @@ export default function JustifiedGallery({
|
|||||||
// Estimate the row width if we were to keep targetH
|
// Estimate the row width if we were to keep targetH
|
||||||
const estimatedWidth = aspectSum * targetH + gap * (current.length - 1);
|
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 we've filled the row (or reached max items) and have at least 2 tiles, flush.
|
||||||
if (
|
if (
|
||||||
(estimatedWidth >= available || current.length >= maxItems) &&
|
(estimatedWidth >= available || current.length >= maxItems) &&
|
||||||
current.length > 1
|
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}`;
|
return `${first}-${last}-${row.length}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isSmallScreen = containerWidth < 640;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn("mx-auto w-full max-w-6xl", className)}
|
className={cn("mx-auto w-full max-w-6xl", className)}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{rows.map((row) => (
|
{rows.map((row, idx) => (
|
||||||
<div
|
<div key={getRowKey(row)}>
|
||||||
key={getRowKey(row)}
|
<div
|
||||||
className="flex justify-center"
|
className={cn(
|
||||||
style={{ gap }}
|
"flex",
|
||||||
>
|
row.length === 1 && (isSmallScreen || idx !== rows.length - 1)
|
||||||
{row.map((t) => (
|
? "justify-center"
|
||||||
<GalleryTile
|
: idx === rows.length - 1
|
||||||
key={t.item.id}
|
? "justify-start"
|
||||||
tile={t}
|
: "justify-between",
|
||||||
hrefBase={hrefBase}
|
)}
|
||||||
hrefFrom={hrefFrom}
|
style={{ columnGap: gap }}
|
||||||
showCaption={showCaption}
|
>
|
||||||
/>
|
{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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -90,17 +90,17 @@ export default function PortfolioGallery({
|
|||||||
dominantHex: it.dominantHex,
|
dominantHex: it.dominantHex,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (items.length === 0) return;
|
// if (items.length === 0) return;
|
||||||
// Debug: inspect dominantHex values coming from the server.
|
// // Debug: inspect dominantHex values coming from the server.
|
||||||
console.log(
|
// console.log(
|
||||||
"[PortfolioGallery] dominantHex sample",
|
// "[PortfolioGallery] dominantHex sample",
|
||||||
items.slice(0, 5).map((it) => ({
|
// items.slice(0, 5).map((it) => ({
|
||||||
id: it.id,
|
// id: it.id,
|
||||||
dominantHex: it.dominantHex,
|
// dominantHex: it.dominantHex,
|
||||||
}))
|
// }))
|
||||||
);
|
// );
|
||||||
}, [items]);
|
// }, [items]);
|
||||||
|
|
||||||
if (!loading && done && galleryItems.length === 0) {
|
if (!loading && done && galleryItems.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -116,6 +116,7 @@ export default function PortfolioGallery({
|
|||||||
items={galleryItems}
|
items={galleryItems}
|
||||||
hrefFrom="portfolio"
|
hrefFrom="portfolio"
|
||||||
showCaption={false}
|
showCaption={false}
|
||||||
|
debug={false}
|
||||||
targetRowHeight={160}
|
targetRowHeight={160}
|
||||||
targetRowHeightMobile={160}
|
targetRowHeightMobile={160}
|
||||||
maxRowHeight={300}
|
maxRowHeight={300}
|
||||||
|
|||||||
Reference in New Issue
Block a user