Руководство по созданию веб-сайта с изменяющимся градиентом фона.
Как начинающий фронтенд-разработчик, вы, возможно, ищете проект для портфолио, чтобы продемонстрировать свои навыки фронтенда. 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.