Build An eCommerce Color Search Tool With NodeJS + React | Part 2
In Part 1 of this series we created the server side logic for a color search tool using Node.js. In Part 2, we will create a user interface, using React, that consumes this data and filters the original list of images. A user will be able to select a color from our predetermined palette and the UI will display only the images that contain x
percent of that color. The value of x
is a threshold we can adjust, but in this example I choose 25%.
Check out this project on Code Sandbox
Getting Started
The eventual output of our server side application from Part 1 was a JSON file, containing the image color analysis data, that we will need for this part of the tutorial. The original images are also required. Even if you missed Part 1, you can follow Part 2 by downloading both links below.
Download Image Analysis JSON File
Head over to CodeSandbox and create a new project using the React template.
Preparing The Sandbox
There are a few things we need to do to prepare our new sandbox to run our color search tool code.
- change the
css
file extension toscss
- unzip the images and upload them to the
public
folder - create
data.js
and copy the image analysis into it - create
palette.js
and copy the color palette into it - be sure to use
export default
on these two files - add
bootstrap@5.0.1
as a dependency
Edit
I ended up adding an additional four colors to the palette used in Part 1 of this tutorial. I did this because some of the lighter shades were closer to white than their actual color.
export default [
{ name: 'black', color: { R: 0, G: 0, B: 0 } },
{ name: 'gray', color: { R: 128, G: 128, B: 128 } },
{ name: 'silver', color: { R: 192, G: 192, B: 192 } },
{ name: 'white', color: { R: 255, G: 255, B: 255 } },
{ name: 'floral_white', color: { R: 255, G: 250, B: 240 } },
{ name: 'beige', color: { R: 245, G: 245, B: 220 } },
{ name: 'tan', color: { R: 218, G: 200, B: 160 } },
{ name: 'purple', color: { R: 103, G: 53, B: 126 } },
{ name: 'navy', color: { R: 19, G: 43, B: 83 } },
{ name: 'blue', color: { R: 31, G: 94, B: 158 } },
{ name: 'peacock', color: { R: 0, G: 128, B: 128 } },
{ name: 'aqua', color: { R: 85, G: 194, B: 195 } },
{ name: 'light_blue', color: { R: 144, G: 193, B: 228 } },
{ name: 'light_cyan', color: { R: 224, G: 255, B: 255 } },
{ name: 'dark_green', color: { R: 32, G: 75, B: 33 } },
{ name: 'green', color: { R: 36, G: 138, B: 15 } },
{ name: 'olive', color: { R: 128, G: 128, B: 0 } },
{ name: 'pale_green', color: { R: 163, G: 176, B: 133 } },
{ name: 'gold', color: { R: 222, G: 170, B: 13 } },
{ name: 'yellow', color: { R: 255, G: 210, B: 70 } },
{ name: 'lavender', color: { R: 230, G: 230, B: 250 } },
{ name: 'brown', color: { R: 79, G: 41, B: 7 } },
{ name: 'burgundy', color: { R: 117, G: 15, B: 23 } },
{ name: 'terracotta', color: { R: 178, G: 77, B: 56 } },
{ name: 'coral', color: { R: 247, G: 117, B: 100 } },
{ name: 'peach', color: { R: 251, G: 189, B: 147 } },
{ name: 'orange', color: { R: 250, G: 118, B: 10 } },
{ name: 'taupe', color: { R: 176, G: 156, B: 130 } },
{ name: 'red', color: { R: 192, G: 7, B: 24 } },
{ name: 'pink', color: { R: 235, G: 111, B: 164 } },
];
Writing The Code
This simple user interface will consist of two major parts:
- A grid of small clickable squares that represent our color palette
- A grid of larger images that represent our products or filterable items
import { useState } from "react";
import DATA from "./data";
import PALETTE from "./palette";
import "bootstrap/dist/css/bootstrap.min.css";
import "./styles.scss";
function formatColorName(name) {
return name
.replace("_", " ")
.split(" ")
.map((el) => el[0].toUpperCase() + el.slice(1))
.join(" ");
}
export default function App() {
const [color, setColor] = useState("");
function colorFilter(x) {
return color ? x.colorAnalysis[color] > 25 : true;
}
function colorSort(a, b) {
return color ? b.colorAnalysis[color] - a.colorAnalysis[color] : 1;
}
return (
<div className="container my-5">
<div className="row">
<div className="col-xl-3">
<div className="color-tool">
{PALETTE.map(({ name, color: { R, G, B } }) => (
<div
key={formatColorName(name)}
style={{ backgroundColor: `rgb(${R},${G},${B})` }}
className="square"
onClick={() => setColor(name)}
/>
))}
<div className="square" onClick={()=>setColor('')}>X</div>
</div>
{color && <div>{formatColorName(color)}</div>}
</div>
<div className="col-xl-9">
<div className="products">
{DATA.filter(colorFilter)
.sort(colorSort)
.map((d) => (
<div key={d.relativePath} className="product">
<img
src={d.relativePath}
alt={d.relativePath}
/>
{color && <div className='value'>{d.colorAnalysis[color]}</div>}
</div>
))}
</div>
</div>
</div>
</div>
);
}
Key Takeaways
In my original implementation, colors names were formatted using snake case (underscores replace spaces). This worked well with my database model but needs to be converted to standard case to be displayed in a UI. The formatColorName
function accomplishes this conversion by chaining JavaScript string and array methods.
The App
component UI is controlled by once state
property for color
, which holds the name of one of the colors from the color palette or an empty string. By mapping the PALETTE
we are able to pass this color name to the setColor
function and assign this callback as the onClick
event handler. Now clicking on a color square will set the name of the color to the state variable color
.
Since our React user interface will re-render when the value of color
changes all we have to do is make the display of images (or imaginary products) depend on this value. In my opinion, this is basic ability is what makes React such a developer friendly tool. In this example we will chain a JavaScript filter
method that uses color
in its filtering logic to accomplish this. In a real world application the value of color
would most likely be used in an API request to fetch the desired data. I chose a reasonable threshold of 25, meaning an image must be composed of at least 25% of the selected color to appear in the filtered results.
I also used a sort
function to make the images appear in descending order based on the percentage of the selected color.
Style
This project is meant to be a simple and minimalistic demonstration of the filtering functionality. However, I included a small scss
file, primarily to style the grids.
body {
overflow-y: scroll;
font-family: monospace;
}
.color-tool {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-gap: 5px;
.square {
width: 40px;
height: 40px;
display: grid;
place-items: center center;
border: 1px solid lightgray;
cursor: pointer;
}
}
.products {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 50px;
.product {
display: grid;
place-items: center center;
img {
width: calc(225px * .75);
height: auto;
}
.value {
font-size: 18px;
font-weight: bold;
margin-top: 5px;
}
}
}
@media only screen and (max-width: 576px) {
.color-tool {
grid-template-columns: repeat(5, 1fr);
margin-bottom: 25px;
}
.products {
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
.product {
img {
width: 40vw;
}
}
}
}
Final Thoughts
I hope this two part series gives readers a good starting point for implementing a similar feature in one of their projects. I tried to make this example as simple as I could. Sometimes if a tutorial is too specific, it can be hard to see how it might apply to your tech stack of choice. Here are a couple ideas to take this example project to the next level.
- connect the server logic to a database to persist data
- add color level sliders to the React UI
- filter by multiple colors
- add the ability for the user to upload an image for analysis