Skip to content

Commit f96309c

Browse files
FayedamzFayedamz
andauthored
fix(web): lazy-load campaign detail images on mobile (#810)
Co-authored-by: Fayedamz <2.95558173e+08+Fayedamz@users.noreply.github.com>
1 parent c43e091 commit f96309c

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { render } from "@testing-library/react";
2+
import { describe, expect, it, vi } from "vitest";
3+
import CampaignImageGallery from "./CampaignImageGallery";
4+
5+
vi.mock("next/image", () => ({
6+
default: (props: Record<string, unknown>) => <img {...props} />,
7+
}));
8+
9+
describe("CampaignImageGallery", () => {
10+
it("loads only the hero eagerly and lazy-loads the remaining images", () => {
11+
const { getAllByRole } = render(
12+
<CampaignImageGallery
13+
images={[
14+
{ src: "/hero.jpg", alt: "Campaign hero" },
15+
{ src: "/detail.jpg", alt: "Campaign detail" },
16+
{ src: "/impact.jpg", alt: "Campaign impact" },
17+
]}
18+
/>,
19+
);
20+
21+
const images = getAllByRole("img");
22+
23+
expect(images[0].getAttribute("loading")).toBe("eager");
24+
expect(images[0].getAttribute("fetchpriority")).toBe("high");
25+
expect(images[1].getAttribute("loading")).toBe("lazy");
26+
expect(images[1].getAttribute("fetchpriority")).toBe("auto");
27+
expect(images[2].getAttribute("loading")).toBe("lazy");
28+
expect(images[2].getAttribute("fetchpriority")).toBe("auto");
29+
});
30+
31+
it("does not render an empty gallery", () => {
32+
const { queryByTestId } = render(<CampaignImageGallery images={[]} />);
33+
34+
expect(queryByTestId("campaign-image-gallery")).toBeNull();
35+
});
36+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
5+
export type CampaignImage = {
6+
src: string;
7+
alt: string;
8+
};
9+
10+
type CampaignImageGalleryProps = {
11+
images: CampaignImage[];
12+
};
13+
14+
/**
15+
* Renders campaign detail images without requesting the whole gallery during
16+
* the initial mobile render. The first image is the above-the-fold hero;
17+
* every following image is below the fold and uses the browser's native lazy
18+
* loading path.
19+
*/
20+
export function CampaignImageGallery({ images }: CampaignImageGalleryProps) {
21+
if (images.length === 0) return null;
22+
23+
return (
24+
<section aria-label="Campaign images" data-testid="campaign-image-gallery">
25+
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
26+
{images.map((image, index) => {
27+
const isHero = index === 0;
28+
29+
return (
30+
<figure
31+
className={
32+
isHero
33+
? "relative col-span-2 aspect-[16/9] overflow-hidden rounded-xl md:col-span-3"
34+
: "relative aspect-square overflow-hidden rounded-xl"
35+
}
36+
key={`${image.src}-${index}`}
37+
>
38+
<Image
39+
src={image.src}
40+
alt={image.alt}
41+
fill
42+
sizes={
43+
isHero
44+
? "(max-width: 767px) 100vw, 100vw"
45+
: "(max-width: 767px) 50vw, 33vw"
46+
}
47+
className="object-cover"
48+
priority={isHero}
49+
loading={isHero ? "eager" : "lazy"}
50+
fetchPriority={isHero ? "high" : "auto"}
51+
decoding="async"
52+
unoptimized
53+
/>
54+
</figure>
55+
);
56+
})}
57+
</div>
58+
</section>
59+
);
60+
}
61+
62+
export default CampaignImageGallery;

0 commit comments

Comments
 (0)