๐Ÿงช Challenges

Pinterest Board

Challenge 5 - Medium

Description

Create a pinterest board for images that shows the images in a grid like the following:

Pinterest Board Grid

The number in the images is the order of insertion. This is going to depend on the height of each column, so we add images in the column with the smallest height.

For example, the first 5 images will be placed side by side on the top (since each column starts with height 0). After that, each new image is placed in whichever column currently has the smallest cumulative height.

Requirements:

  • We need 5 columns in total.
  • We don't want it to be responsive (always 5 columns).
  • The images should be 200px width.
  • A new image should be added in the column with less height.

1. Board - Implementation

Interface

We define an ImageDataTypes interface with fields (url, width, height, index) to keep track of each image. We are going to save the index to show as a label on the image.

0 1 2 3 4 5 export type ImageDataTypes = { url: string; height: number; width: number; index: number; };

State

The board will have the following state:

  • A list of the image data that we are going to use.
  • A matrix with that is going to represent the board, being each column a column in the board.
  • An array of columnHeights, used to calculate where to put the next image.
0 1 2 3 const [images, setImages] = React.useState<ImageDataTypes[]>([]); const [columnsArray, setColumsArray] = React.useState<ImageDataTypes[][]>([[],[],[],[],[]]); const [columnHeights, setColumnHeights] = React.useState<number[]>([0,0,0,0,0]);

Initialization

We use the useEffect to call initializeImages just once to populate images with random widths/heights from the service loremflickr.com. The service is free and do not work properly all the time.

0 1 2 3 4 5 6 7 8 9 10 const initializeImages = () => { let images: ImageDataTypes[] = []; for (let index = 0; index < 20; index++) { const randomWidth = Math.floor(Math.random() * 200) + 200; const randomHeight = Math.floor(Math.random() * 300) + 200; const imageUrl = `https://loremflickr.com/${randomWidth}/${randomHeight}`; images.push({ url: imageUrl, height: randomHeight, width: randomWidth, index }); } setImages(images); };

Distribution Logic

On every change of images, a new layout is computed:

  1. Loop each image.
  2. Find the column with the smallest height.
  3. Push the image to that column.
  4. Update columnsArray and columnHeights together for consistency.
0 1 2 3 4 5 6 7 8 9 10 11 12 useEffect(() => { const newColumnsArray: ImageDataTypes[][] = [[], [], [], [], []]; const newColumnHeights: number[] = [0, 0, 0, 0, 0]; images.forEach((image) => { const minIndex = newColumnHeights.indexOf(Math.min(...newColumnHeights)); newColumnsArray[minIndex].push(image); newColumnHeights[minIndex] += image.height; }); setColumsArray(newColumnsArray); setColumnHeights(newColumnHeights); }, [images]);

Render

The board component renders the columns in a loop, passing the images to each column.

0 1 2 3 4 5 6 7 return ( <div className={styles.board}> {columnsArray.map((column, index) => ( <PinterestColumn key={index} images={column} /> ))} </div> );

In terms of styles, there are multiple approaches that can be used to render the board but with this design we can use a simple flexbox layout to arrange the columns and images.

0 1 2 3 4 5 6 7 .board { position: relative; display: flex; flex-direction: row; justify-content: flex-start; gap: 10px; }

2. Column - Implementation

Rendering Images

Each column is a simple component that maps through the images that recieves as a paramenter. It renders them in order, preserving the original insertion order to create a stacked look.

In this case we are commenting the img tag because the image service that I'm using is not working properly, so we are showing a placeholder.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <div className={styles.column}> { images.map((image, index) => { return ( <div key={index} className={styles.imageContainer} style={{ width: '200px', height: image.height }} > <p className={styles.index}>{image.index}</p> {/* The image service that I'm using is not working properly. <img key={index} className={styles.image} src={image.url} alt={`Image ${index}`} /> */} </div> ); }) } </div>

The styles of this component are very simple, we can use a simple flexbox to show them.

0 1 2 3 4 5 6 7 .column { width: 200px; display: flex; flex-direction: column; justify-content: flex-start; gap: 10px; }

If we need, we could calculate the aspect ratio of the images in the component and use it to set the height of the images.

3. Example

4. Complete Code

Board

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 'use client' import React, { useEffect } from 'react'; import styles from './PinterestBoard.module.css'; import PinterestColumn from './PinterestColumn'; export type ImageDataTypes = { url: string; height: number; width: number; index: number; }; const IMAGE_URL = "https://loremflickr.com/"; const PinterestBoard = () => { const [images, setImages] = React.useState<ImageDataTypes[]>([]); const [columnsArray, setColumsArray] = React.useState<ImageDataTypes[][]>([[],[],[],[],[]]); const [columnHeights, setColumnHeights] = React.useState<number[]>([0,0,0,0,0]); // Initializes the data of the images that we are going to use const initializeImages = () => { // Generate 20 images with random height and width let images: ImageDataTypes[] = []; for (let index = 0; index < 20; index++) { const randomWidth = Math.floor(Math.random() * 200) + 200; const randomHeight = Math.floor(Math.random() * 300) + 200; const imageUrl = `${IMAGE_URL}/${randomWidth}/${randomHeight}`; images.push({ url: imageUrl, height: randomHeight, width: randomWidth, index }); } setImages(images); }; // Initialize images once useEffect(() => { initializeImages(); }, []); useEffect(() => { // Prepare new columns and heights from scratch const newColumnsArray: ImageDataTypes[][] = [[], [], [], [], []]; const newColumnHeights: number[] = [0, 0, 0, 0, 0]; // Distribute each image by the current min column images.forEach((image) => { const minIndex = newColumnHeights.indexOf(Math.min(...newColumnHeights)); newColumnsArray[minIndex].push(image); newColumnHeights[minIndex] += image.height; }); // Now update our state in a single batch setColumsArray(newColumnsArray); setColumnHeights(newColumnHeights); }, [images]); return ( <div className={styles.board}> {columnsArray.map((column, index) => ( <PinterestColumn key={index} images={column} /> ))} </div> ); }; export default PinterestBoard;

Column

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React, { Suspense } from 'react'; import styles from './PinterestColumn.module.css'; import { ImageDataTypes } from './PinterestBoard'; interface PinterestColumnPropType { images: ImageDataTypes[]; } const PinterestColumn: React.FC<PinterestColumnPropType> = ({ images }) => { return ( <div className={styles.column}> { images.map((image, index) => { return ( <div key={index} className={styles.imageContainer} style={{ width: '200px', height: image.height }} > <p className={styles.index}>{image.index}</p> {/* The image service that I'm using is not working properly. <img key={index} className={styles.image} src={image.url} alt={`Image ${index}`} /> */} </div> ); }) } </div> ); }; export default PinterestColumn;
๐Ÿ›๏ธ ๐Ÿงฎ ๐Ÿ“ โš›๏ธ ๐Ÿงช ๐Ÿ