Pinterest Board
Challenge 5 - Medium
Description
Create a pinterest board for images that shows the images in a grid like the following:
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:
- Loop each image.
- Find the column with the smallest height.
- Push the image to that column.
- 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;