⚛️React.js + 🌈Gradients = Gradientti
Tags: react, tailwindcss, webdev
Published: 2022-12-18 02:35:14

As a newbie frontend developer, you might be looking for a portfolio project to show case your front-end skills. Gradientti is a simple app which does this - you can view css color gradients, make your own and also copy the generated css code.

**Details about the App:** [Live Link](https://elitenoire.github.io/gradientti/) | [Github Link](https://github.com/elitenoire/gradientti)

The Gradientti app is built using `React.js` and `Tailwindcss` to make development faster and easier.

**Note:** this guide only focuses on the core logic / features of the app and leaves the setup and css styles out (as there are many articles on the dev stack).

## Section Guide
* [01. The App Structure](#section-1)
* [02. The `useSequence` Hook](#section-2)
* [03. Putting it Together](#section-3)
* [04. Displaying the `gradientList` in the Sidebar](#section-4)
* [05. Adding a New Gradient Swatch](#section-5)
* [06. Copying the CSS Gradient Code](#section-6)

---

## 1. The App Structure <a name="section-1" />
The App is divided into four main components namely:

- **The Header**
![Gradientti Header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhthpzw00lc59egizkvd.png)  The `Header` component consists of the App Logo, a color panel displaying the gradient colors, a button to copy the css gradient code, a button to add new gradients and a button to view all gradients.


- **The Background**
![Gradientti Background](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j911mi8re621q1hs99bq.png)  The `Background` which is the main body of the app has the css gradient background and button controls to cycle forwards or backwards through the list of available gradients.  
  

- **The Sidebar**
![Gradientti Sidebar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5xclhfnbdu9ltf4prd9z.png)    The `Sidebar`'s visibility is controlled by a button. It is like a select menu to view all the gradient swatches and for quick navigation.
 

- **The Footer**
![Gradientti Footer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98uhywm6g3153r6xs3nh.png)  The `Footer` which is optional but nice to have.

  

## 2. The `useSequence` Hook <a name="section-2" />
The `useSequence` hook is the engine behind changing the css background gradients. It provides functions to cycle through the list of gradients backwards / forwards or going to a particular index. It uses React's `useReducer` state pattern to organize the logic.

```js
// reducer actions
const INC = 'INCREMENT'
const DEC = 'DECREMENT'
const GOTO = 'GOTO'
const SYNC = 'SYNC'

// hook to provide cycling logic through the list
const useSequence = ({ count, direction = 0, start = 0, end = 4 }) => {
  const defaultCount = count || start
  const initialState = { count: defaultCount, defaultCount, direction, start, end }

  const [state, dispatch] = useReducer(reducer, initialState)

  const increment = useCallback(() => dispatch({ type: INC }), [dispatch])
  const decrement = useCallback(() => dispatch({ type: DEC }), [dispatch])
  const goto = useCallback(index => dispatch({ type: GOTO, index }), [dispatch])
  const sync = useCallback((index, bounds = 'end') => dispatch({ type: SYNC, bounds, index }), [dispatch])

  return {
    ...state,
    increment,
    decrement,
    goto,
    sync,
  }
}
```
As part of the `reducer state`,

- the `start` and `end` state defines the min and max bounds the reducer will cycle through. If `start = 0` and `end = 4`, the cycle will be `0 -> 1 -> 2 -> 3 -> 4` and repeats.
- `count` represents the current position / index of the cycle.
- `direction` - could be `-1`, `0` or `1` to determine if we are moving backwards, static or forwards.

The below reusable functions will dispatch `actions` that will update our `state` if necessary,

- `increment()` will cycle forwards while `decrement()` will cycle backwards
- `goto(index)` will cycle to a particular index
- `sync(index, bounds: end | start)` will update our `start` or `end` bounds to the particular index. This will be useful when dynamically setting the bounds as in the case of adding a new gradient to our predefined gradient list. 

```js
// state reducer
const reducer = (state, { type, bounds, index }) => {
  const { count, start, end } = state
  const total = end - start + 1

  switch (type) {
    case INC:
      return {
        ...state,
        direction: 1,
        count: (count + 1 + total) % total,
      }
    case DEC:
      return {
        ...state,
        direction: -1,
        count: (count - 1 + total) % total,
      }
    case GOTO:
      return { ...state, direction: 0, count: clamp(index, start, end) }
    case SYNC:
      return {
        ...state,
        [bounds]: index,
      }
    default:
      return state
  }
}
```

The `clamp` utility function is an extra check / guard to prevent our sequencer from cycling out of the `start` and `end` bounds.

```js
export const clamp = (num, lower, upper) => (upper ? Math.min(Math.max(num, lower), upper) : Math.min(num, lower))

```

## 3. Putting It Together <a name="section-3" />
The list of gradients is stored as an array of objects in javascript. For example,

```js
const gradients = [
 {
  name: 'Cosmic Tail',
  start: '#780206',
  end: '#061161',
 },
 {
  name: 'Berry Bloom',
  start: '#FBD3E9',
  end: '#BB377D',
 },
 ...
]
```
This will be stored in React's state so that we can iterate through it and update our app's UI whenever changes are made to it.

The last gradient in our list will serve as the `end` bounds of our `useSequence` hook which generates our current index (`count`) in the gradient list. The `increment()` and `decrement()` functions will be assigned to the `onClick` event handler of our navigation buttons.

```js
import gradients from './gradients'

const [gradientList, setGradientList] = useState(gradients)
const { count, increment, decrement } = useSequence({
    end: gradientList.length - 1,
  })
const { name, start, end } = gradientList[count]

// pseudo code
return (
  ...
  
  <PrevButton onClick={decrement} />
  <NextButton onClick={increment} />
  <GradientBackground style={{ backgroundImage: `linear-gradient(to right, ${start}, ${end})` }}>{name}</GradientBackground>
  
  ...
)

```

## 4. Displaying the `gradientList` in the Sidebar <a name="section-4" />
In this part, we want a button to toggle open the sidebar which displays our list of available gradients in a grid. Whenever a gradient swatch is selected, our gradient background should update to the selected gradient.

Luckily, we can use the `Listbox` and `Transition` components from the [@headlessui/react](https://headlessui.com/react/listbox) package to create an animated sidebar.

Because the `Listbox` manages the selection internally, we make it a controlled component by providing it `value` and `onChange` props so that the selected gradient can be available to our app's state and UI.

```js
// pseudo code
const { count, goto } = useSequence({
    end: gradientList.length - 1,
  })
const gradient = gradientList[count]

const handleChange = useCallback(
  selected => {
    goto(selected)
  },
  [goto]
)

<ListBox value={count} onChange={handleChange}>
  ...
<ListBox>
```

We pass the current index (`count`) as the `value` of the ListBox. Whenever the internal value changes, the `handleChange()` callback is called and passed the new value. We utilize the `goto()` function from the `useSequencer` hook to navigate to the new value which becomes the current index in our gradient list.

We also map through the gradient list and wrap each gradient swatch with the `Listbox.Options` component. The index of each child is used as the `value` of the component.

```js
// pseudo code from GradientsView.js
<Listbox value={count} onChange={handleChange}>
    <Lisbox.Button />
    <Listbox.Options>
        {gradientList.map(({name,start,end}, i) => {
            <Listbox.Option key={name} value={i}>
                <GradientSwatch style={{backgroundImage: `linear-gradient(to right, ${start}, ${end})`}}>
                    {name}
                </GradientSwatch>
            </Listbox.Option>
        })}
    </Listbox.Options>
</Listbox.Options>
```

## 5. Adding a new Gradient Swatch <a name="section-5" />
A nice feature to have in our gradient background changing app is the ability to add new gradient swatches to the default gradient list. The add button in our `Header` component will be responsible for toggling open a modal to display a form which can be used to add new gradients.

![Add New Gradient Form Modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/khag4jx44pp8myb5j3b5.png)

To help build this feature, we will use the `Dialog` component from [@headlessui/react](https://headlessui.com/react/dialog) package to create the modal, the hex color input and picker from [react-colorful](https://github.com/omgovich/react-colorful) package and the `@tailwindcss/forms` plugin to reset html form input styles.

We have our `formGradientState` which holds the values for each form input - `start`, `end` and `name` and the `errorsState` for validation and submission errors.

```js
// initialGradient from the App's State to initialize form inputs
const [newGradient, setNewGradient] = useState(initialGradient)
const [errors, setErrors] = useState({})

const { start, end, name } = newGradient
const { gradient: errGradient, name: errName } = errors
```

The `HexColorPicker` and `HexColorInput` from `react-colorful` have an internally managed state in which we can access the selected color through their `onChange` callback function. The components have in-built validation (prevent empty inputs, incorrect hex colors etc) hence error handling is done only for form submission. 

```js
// pseudo code
const color: start | end

// handler for both start and end colors
const handleColorChange = useCallback(
    key => color => {
      setNewGradient(_gradient => ({ ..._gradient, [key]: color 
    }))
      // clear errors on color input change
      setErrors(_errors => ({ ..._errors, gradient: '' }))
    },
    []
)
...
<HexColorInput color={color} onChange={handleColorChange('start')} />
<HexColorPicker color={color} onChange={handleColorChange('start')} />
...
{errGradient && <ErrorText text="Gradient already exists."/>
```

In contrast, the form input for the gradient name uses the html client-side validation to guard against errors.

```js
const handleNameChange = useCallback(({ target }) => {
    target.setCustomValidity('')
    setNewGradient(_gradient => ({ ..._gradient, name: 
    target.value }))
    setErrors(_errors => ({ ..._errors, name: '' }))
}, [])

const handleNameValidity = useCallback(({ target }) => {
    if (target.validity.valueMissing) {
      target.setCustomValidity('Name is required.')
    } else if (target.validity.patternMismatch) {
      target.setCustomValidity('Name is invalid. Use 2 or more 
      letters.')
    }
}, [])

...
<input
    value={name}
    onChange={handleNameChange}
    onInvalid={handleNameValidity}
    type="text"
    pattern="[a-zA-Z]+[\s]?[A-Za-z]+"
    required
/>
{errName && <ErrorText text="Name already exists."/>
...

```
The `ErrorText` component displays errors from the form submission. When we want to add a new gradient, we first check if that gradient swatch does not exist in our gradient list before submitting. 

```js
// utility to check if new gradient is in our gradient list
const checkIfExist = (list, item) => {
  const nameExist = list.some(_item => _item.name === item.name)
  const gradientExist = list.some(_item => _item.start === 
  item.start && _item.end === item.end)
  return { gradientExist, nameExist }
}

const handleSubmit = useCallback(
    e => {
      e.preventDefault()
      const { gradientExist = '', nameExist = '' } = 
      checkIfExist(gradientList, newGradient)
      setErrors({ name: nameExist, gradient: gradientExist })
      if (!(gradientExist || nameExist)) {
        onMake?.(newGradient)
      }
    },
    [gradientList, newGradient, onMake]
)

```
The `onMake` callback sends the new gradient to our App when all validation has been done successfully during submission.

In our App, we use the `handleGradientAdd` callback to retrieve the new gradient and add it to our gradient list. The new gradient is inserted at the index after our currently displayed gradient. We also update our `end` bounds to accommodate the additional gradient and then navigate to the new gradient using `increment()`.

```js
const lastGradientIndex = gradientList.length - 1
const lastGradientIndexRef = useRef(lastGradientIndex)
const { count, increment, sync } = useSequence({
    end: lastGradientIndex,
})

const handleGradientAdd = useCallback(
    newGradient => {
      setGradientList(list => {
        lastGradientIndexRef.current = list.length
        return insertAt(list, count + 1, newGradient)
      })
      sync(lastGradientIndexRef.current)
      increment()
    },
    [count, increment, sync]
)
```

## 6. Copying the CSS Gradient Code <a name="section-6" />
Another useful feature to have in the app is for anyone to copy the css gradient code easily. When we click the copy button in the `Header` component, we open the modal with the css gradient code block for the currently displayed gradient background.

![CSS Copy Modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/720joleq8b2ey2bcxeql.png)

In the modal, there's a vendor prefix checkbox to toggle browser compatibility code and the button to copy the css code. This is easily achieved using the `useClipboard` hook from the [use-clipboard-copy](https://github.com/wsmd/use-clipboard-copy) package and the [prismjs](https://prismjs.com/) package for css syntax highlighting.

```js
import { useEffect } from 'react'
import { highlightAll } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-css'
import 'prismjs/plugins/line-numbers/prism-line-numbers'
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
import 'prismjs/plugins/normalize-whitespace/prism-normalize-whitespace'

function CodeBlock({ code }) {
  useEffect(() => {
    highlightAll()
  }, [code])

  return (
    <div className="overflow-auto">
      <pre className="language-css line-numbers">
        <code>{code}</code>
      </pre>
    </div>
  )
}

```

```js
import { useState, useCallback } from 'react'
import { useClipboard } from 'use-clipboard-copy'

// pseudo code
const code: prefixedCss | normalCss

const [prefix, setPrefix] = useState(true)
const { copy, copied } = useClipboard({
    copiedTimeout: 2000,
})

const handleChange = useCallback(e => {
    setPrefix(e.target.checked)
}, [])
const handleCopy = useCallback(() => {
    copy(code)
}, [copy, code])

return (
  ...
  <Codeblock code={code} />
  ...
  <input type="checkbox" checked={prefix} onChange= 
  {handleChange}/>
  <button onClick={handleCopy}>Copy CSS</button>
  ...
)
```
---

In conclusion, we've highlighted the logic and features found in our gradient changing background app. The full code is accessible at the [github repo](https://github.com/elitenoire/gradientti). Please don't forget to star the repo if you found it useful.