Skip to content

Commit

Permalink
feat(gallery): add masonry gallery view
Browse files Browse the repository at this point in the history
Refs: IS-7
  • Loading branch information
ythepaut committed Aug 15, 2024
1 parent 15ea4cd commit 52f27a5
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ APP_URL=https://showcase.ythepaut.com/

TIMEZONE=Europe/Paris
DEFAULT_LOCALE=fr

IMAGE_HOSTNAME_PATTERN=cdn.pixabay.com
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ APP_URL=https://showcase.ythepaut.com/

TIMEZONE=Europe/Paris
DEFAULT_LOCALE=fr

IMAGE_HOSTNAME_PATTERN=cdn.pixabay.com
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ cypress/videos/

.swc/

ignore/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ythepaut/image-showcase/test.yml?style=for-the-badge)](https://github.com/ythepaut/image-showcase/actions)
[![Sonar Quality Gate](https://img.shields.io/sonar/quality_gate/ythepaut_image-showcase?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge)](https://sonarcloud.io/project/overview?id=ythepaut_image-showcase)
[![Sonar Coverage](https://img.shields.io/sonar/coverage/ythepaut_image-showcase?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge)](https://sonarcloud.io/project/overview?id=ythepaut_image-showcase)
[![Live Demo](https://img.shields.io/website?down_color=red&down_message=Unavailable&label=Live%20Website&style=for-the-badge&up_color=green&up_message=Online&url=https%3A%2F%2Fshowcase.ythepaut.com%2F1 )](https://showcase.ythepaut.com/)
[![Live Demo](https://img.shields.io/website?down_color=red&down_message=Unavailable&label=Live%20Demo&style=for-the-badge&up_color=green&up_message=Online&url=https%3A%2F%2Fshowcase.ythepaut.com)](https://showcase.ythepaut.com/)

Image gallery

Expand Down
6 changes: 5 additions & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {setConfig} from "next/config";
import { setConfig } from "next/config";
import config from "./next.config";
import { TextDecoder, TextEncoder } from "util";

setConfig(config);

global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
14 changes: 12 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ module.exports = {
appDescription: process.env.APP_DESCRIPTION,
appKeywords: process.env.APP_KEYWORDS,
appUrl: process.env.APP_URL,
timeZone: process.env.TIMEZONE,
timeZone: process.env.TIMEZONE
},
reactStrictMode: true,
swcMinify: true,
i18n: {
locales: ["fr", "en"],
defaultLocale: process.env.DEFAULT_LOCALE,
defaultLocale: process.env.DEFAULT_LOCALE
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: process.env.IMAGE_HOSTNAME_PATTERN,
port: "",
pathname: "/**"
}
]
}
};
80 changes: 80 additions & 0 deletions public/assets/images.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
[
{
"src": "https://cdn.pixabay.com/photo/2024/08/05/21/19/lion-8947711_960_720.jpg",
"width": 750,
"height": 720,
"alt": "1"
},
{
"src": "https://cdn.pixabay.com/photo/2023/11/26/23/43/horse-8414411_960_720.jpg",
"width": 480,
"height": 720,
"alt": "2"
},
{
"src": "https://cdn.pixabay.com/photo/2018/03/04/17/23/tiger-3198598_960_720.jpg",
"width": 576,
"height": 720,
"alt": "3"
},
{
"src": "https://cdn.pixabay.com/photo/2023/11/01/11/15/cable-car-8357178_960_720.jpg",
"width": 960,
"height": 640,
"alt": "4"
},
{
"src": "https://cdn.pixabay.com/photo/2024/02/21/15/09/plant-8587893_960_720.jpg",
"width": 960,
"height": 638,
"alt": "5"
},
{
"src": "https://cdn.pixabay.com/photo/2016/05/22/19/48/sunrise-1409090_960_720.jpg",
"width": 960,
"height": 540,
"alt": "6"
},
{
"src": "https://cdn.pixabay.com/photo/2023/12/15/03/11/basket-to-the-sea-8449952_960_720.jpg",
"width": 518,
"height": 720,
"alt": "7"
},
{
"src": "https://cdn.pixabay.com/photo/2024/07/19/12/23/woman-8906207_960_720.jpg",
"width": 720,
"height": 720,
"alt": "8"
},
{
"src": "https://cdn.pixabay.com/photo/2024/08/05/13/17/dog-8946829_960_720.jpg",
"width": 960,
"height": 640,
"alt": "9"
},
{
"src": "https://cdn.pixabay.com/photo/2024/05/24/19/06/bird-8785666_960_720.jpg",
"width": 540,
"height": 720,
"alt": "10"
},
{
"src": "https://cdn.pixabay.com/photo/2024/07/05/22/30/penguin-8875750_960_720.jpg",
"width": 960,
"height": 640,
"alt": "11"
},
{
"src": "https://cdn.pixabay.com/photo/2024/01/02/10/33/stream-8482939_960_720.jpg",
"width": 480,
"height": 720,
"alt": "12"
},
{
"src": "https://cdn.pixabay.com/photo/2024/07/15/16/09/bird-8897237_960_720.jpg",
"width": 960,
"height": 640,
"alt": "13"
}
]
1 change: 0 additions & 1 deletion public/lang/en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"home": {
"hello": "Hello"
}
}
1 change: 0 additions & 1 deletion public/lang/fr.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"home": {
"hello": "Bonjour"
}
}
36 changes: 36 additions & 0 deletions src/components/Shimmer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
interface Props {
width: number;
height: number;
}

export default function Shimmer({ width, height }: Readonly<Props>) {
const bgColour = "#EBF2FA";
const waveColour = "#E4ECF7";

return (
<svg
width={width}
height={height}
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<linearGradient id="g">
<stop stopColor={bgColour} offset="30%" />
<stop stopColor={waveColour} offset="50%" />
<stop stopColor={bgColour} offset="70%" />
</linearGradient>
</defs>
<rect width={width} height={height} fill={bgColour} />
<rect id="r" width={width} height={height} fill="url(#g)" />
<animate
xlinkHref="#r"
attributeName="x"
from={-width}
to={width}
dur="1s"
repeatCount="indefinite"
/>
</svg>
);
}
46 changes: 46 additions & 0 deletions src/components/gallery/Gallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Image } from "../../model/image";
import ImageTile from "./ImageTile";
import { useEffect, useState } from "react";

const SCREEN_WIDTH_XL = 1280;
const SCREEN_WIDTH_MD = 768;

interface Props {
images: Image[];
}

export default function Gallery({ images }: Readonly<Props>) {
const [columns, setColumns] = useState<number>(1);

// Window width handling for column count
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= SCREEN_WIDTH_XL) setColumns(3);
else if (window.innerWidth >= SCREEN_WIDTH_MD) setColumns(2);
else setColumns(1);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);

// Arranging the images into the columns
// Each image is added to the column with the lowest height
const columnHeights: number[] = Array(columns).fill(0);
const columnWrappers: Image[][] = Array.from({ length: columns }, () => []);
images.forEach((image: Image) => {
const shortestColumnIdx = columnHeights.indexOf(Math.min(...columnHeights));
columnWrappers[shortestColumnIdx].push(image);
columnHeights[shortestColumnIdx] += image.height / image.width;
});

return (
<div
className={`${columns === 1 ? "columns-1" : columns === 2 ? "columns-2" : "columns-3"} gap-0`}
>
{columnWrappers.flat().map((image: Image) => (
<ImageTile image={image} key={image.src} />
))}
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/gallery/ImageTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Image } from "../../model/image";
import { default as NextImage } from "next/image";
import ReactDOMServer from "react-dom/server";
import Shimmer from "../Shimmer";

interface Props {
image: Image;
}

export default function ImageTile({ image }: Readonly<Props>) {
return (
<div className="p-1">
<NextImage
className="w-full"
src={image.src}
alt={image.alt}
width={image.width}
height={image.height}
placeholder={`data:image/svg+xml,${encodeURIComponent(ReactDOMServer.renderToString(<Shimmer width={image.width} height={image.height} />))}`}
/>
</div>
);
}
6 changes: 6 additions & 0 deletions src/model/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Image {
src: string;
alt: string;
width: number;
height: number;
}
24 changes: 17 additions & 7 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import {useTranslations} from "next-intl";
import { GetStaticProps } from "next";
import Gallery from "../components/gallery/Gallery";
import { Image } from "../model/image";

export default function HomePage() {
const t = useTranslations("home");
return <h1 className="text-red-600">{t("hello")}</h1>;
interface Props {
messages: any;
images: Image[];
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
export const getStaticProps: GetStaticProps<Props> = async (context) => {
return {
props: {
messages: require(`../../public/lang/${locale}.json`),
},
messages: require(`../../public/lang/${context.locale}.json`),
images: require("../../public/assets/images.json")
}
};
};

export default function HomePage(props: Readonly<Props>) {
return (
<section className="m-8">
<Gallery images={props.images} />
</section>
);
}

0 comments on commit 52f27a5

Please sign in to comment.