Руководство по созданию веб-сайта с изменяющимся градиентом фона.

Как начинающий фронтенд-разработчик, вы, возможно, ищете проект для портфолио, чтобы продемонстрировать свои навыки фронтенда. Gradientti — это простое приложение, которое делает это — вы можете просматривать цветовые градиенты CSS, создавать свои собственные, а также копировать сгенерированный код CSS.

Подробнее о приложении: Живая ссылка | Ссылка на гитхаб

Приложение Gradientti построено с использованием React.js и Tailwindcss, чтобы ускорить и упростить разработку.

Примечание. это руководство фокусируется только на основной логике/функциях приложения и не включает настройку и стили CSS (поскольку в стеке разработчиков есть много статей).

1. Структура приложения

Приложение разделено на четыре основных компонента, а именно:

  • Заголовок. Компонент Header состоит из логотипа приложения, цветной панели, отображающей цвета градиента, кнопки для копирования кода градиента CSS, кнопки для добавления новых градиентов и кнопки для просмотра всех градиентов.

  • Фон: Background, который является основной частью приложения, имеет градиентный фон CSS и элементы управления кнопками для циклического перехода вперед или назад по списку доступных градиентов.

  • Боковая панель: видимость Sidebar управляется кнопкой. Это похоже на меню выбора для просмотра всех образцов градиента и быстрой навигации.

  • Нижний колонтитул: элемент Footer необязателен, но его желательно иметь.

2. Крюк useSequence

Хук useSequence — это механизм изменения фоновых градиентов CSS. Он предоставляет функции для циклического перемещения по списку градиентов назад/вперед или перехода к определенному индексу. Он использует шаблон состояния React useReducer для организации логики.

import { useReducer } from 'react'

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

// hook to provide cycling logic through the list
function 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,
  }
}

В рамках reducer state,

  • состояния start и end определяют минимальные и максимальные границы, через которые будет проходить редьюсер. Если start = 0 и end = 4, цикл будет 0 -> 1 -> 2 -> 3 -> 4 и повторяется.
  • count представляет текущую позицию/индекс цикла.
  • direction — может быть -1, 0 или 1, чтобы определить, движемся ли мы назад, статично или вперед.

Приведенные ниже многократно используемые функции будут отправлять actions, который при необходимости обновит наш state,

  • increment() будет циклически двигаться вперед, а decrement() — назад.
  • goto(index) будет циклически переходить к определенному индексу
  • sync(index, bounds: end | start) обновит наши границы start или end до определенного индекса. Это будет полезно при динамической установке границ, как в случае добавления нового градиента в наш предопределенный список градиентов.
// state reducer
function 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
  }
}

Вспомогательная функция clamp является дополнительной проверкой/защитой для предотвращения циклического выхода нашего секвенсора за границы start и end.

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

3. Собираем вместе

Список градиентов хранится в виде массива объектов в javascript. Например,

const gradients = [
 {
  name: 'Cosmic Tail',
  start: '#780206',
  end: '#061161',
 },
 {
  name: 'Berry Bloom',
  start: '#FBD3E9',
  end: '#BB377D',
 },
 ...
]

Это будет храниться в состоянии React, чтобы мы могли перебирать его и обновлять пользовательский интерфейс нашего приложения всякий раз, когда в него вносятся изменения.

Последний градиент в нашем списке будет служить end границами нашего useSequence хука, который генерирует наш текущий индекс ( count) в списке градиентов. Функции increment() и decrement() будут назначены обработчику событий onClick наших кнопок навигации.

import gradients from './gradients'

function App() {
    ...
    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. Отображение gradientList на боковой панели

В этой части мы хотим, чтобы кнопка открывала боковую панель, которая отображает наш список доступных градиентов в сетке. Всякий раз, когда выбран образец градиента, наш градиентный фон должен обновляться до выбранного градиента.

К счастью, мы можем использовать компоненты Listbox и Transition из пакета @headlessui/react для создания анимированной боковой панели.

Поскольку Listbox управляет выбором внутри, мы делаем его контролируемым компонентом, предоставляя ему свойства value и onChange, чтобы выбранный градиент мог быть доступен для состояния и пользовательского интерфейса нашего приложения.

import { useCallback } from 'react'
import { Listbox } from '@headlessui/react'

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

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

return (
    <ListBox value={count} onChange={handleChange}>
      ...
    <ListBox>
)

Мы передаем текущий индекс (count) как value ListBox. Всякий раз, когда внутреннее значение изменяется, вызывается обратный вызов handleChange() и передается новое значение. Мы используем функцию goto() из хука useSequencer для перехода к новому значению, которое становится текущим индексом в нашем списке градиентов.

Мы также сопоставляем список градиентов и оборачиваем каждый образец градиента компонентом Listbox.Options. Индекс каждого потомка используется как value компонента.

import { Listbox } from '@headlessui/react'
// 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. Добавление нового образца градиента

Приятной особенностью нашего приложения для изменения фона градиента является возможность добавлять новые образцы градиента в список градиентов по умолчанию. Кнопка добавления в нашем компоненте Header будет отвечать за переключение открытия модального окна для отображения формы, которую можно использовать для добавления новых градиентов.

Чтобы помочь в создании этой функции, мы будем использовать компонент Dialog из пакета @headlessui/react для создания модального окна, шестнадцатеричный ввод цвета и средство выбора из пакета react-colorful, а также плагин @tailwindcss/forms для сброса стилей ввода HTML-формы.

У нас есть formGradientState, который содержит значения для каждого ввода формы — start, end и name, а также errorsState для ошибок проверки и отправки.

import { useState } from 'react'

// 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

HexColorPicker и HexColorInput из react-colorful имеют внутреннее управляемое состояние, в котором мы можем получить доступ к выбранному цвету через их функцию обратного вызова onChange. Компоненты имеют встроенную проверку (предотвращение пустых входных данных, неправильных шестнадцатеричных цветов и т. д.), поэтому обработка ошибок выполняется только для отправки формы.

import { HexColorInput, HexColorPicker } from 'react-colorful'
// 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: '' }))
    },
    []
)
return (
    ...
    <HexColorInput color={color} onChange={handleColorChange('start')} />
    <HexColorPicker color={color} onChange={handleColorChange('start')} />
    ...
    {errGradient && <ErrorText text="Gradient already exists."/>
)

Напротив, ввод формы для имени градиента использует проверку HTML на стороне клиента для защиты от ошибок.

import { useCallback } from 'react'

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.')
    }
}, [])

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

Компонент ErrorText отображает ошибки отправки формы. Когда мы хотим добавить новый градиент, мы сначала проверяем, не существует ли этот образец градиента в нашем списке градиентов перед отправкой.

// utility to check if new gradient is in our gradient list
function 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 }
}

// form submission handler
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]
)

Обратный вызов onMake() отправляет новый градиент в наше приложение, когда все проверки были успешно выполнены во время отправки.

В нашем приложении мы используем обратный вызов handleGradientAdd(), чтобы получить новый градиент и добавить его в наш список градиентов. Новый градиент вставляется по индексу после текущего отображаемого градиента. Мы также обновляем наши границы end, чтобы учесть дополнительный градиент, а затем переходим к новому градиенту, используя increment().

import { useRef } from 'react'

function App() {
...
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. Копирование кода градиента CSS

Еще одна полезная функция в приложении — любой может легко скопировать код градиента CSS. Когда мы нажимаем кнопку копирования в компоненте Header, мы открываем модальное окно с блоком кода градиента CSS для текущего отображаемого градиентного фона.

В модальном окне есть флажок префикса поставщика для переключения кода совместимости браузера и кнопка для копирования кода CSS. Этого легко добиться с помощью хука useClipboard из пакета use-clipboard-copy и пакета prismjs для подсветки синтаксиса CSS.

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>
  )
}
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>
  ...
)

В заключение мы выделили логику и функции нашего фонового приложения с изменяющимся градиентом. Полный код доступен в репозитории GitHub. Пожалуйста, не забудьте пометить репозиторий звездочкой, если он оказался вам полезен.

Первоначально опубликовано на https://elitenoire.hashnode.dev.