Как добавить интерактивный туториал на сайт используя React

Часы
29.08.2024
Часы
17.03.2025
Часы
4 минуты
Глазик
142
Сердечки
0
Соединённые точки
0
Соединённые точки
0
Соединённые точки
0

Вступление или для чего вообще нужен интерактивный туториал

Привет, опять. В этой статье ты узнаешь как быстро и просто создать интерактивный туториал по вашему инструменту на Реакте. И первый вопрос который бы я задал, был бы такой: а нужен ли он вообще?
И действительно, если функционал инструмента максимально простой и интуитивно понятный, то заморачиваться над туториалом не стоит. В идеале, к этому и нужно стремиться чтобы пользователь без всякой лишней мысли приступал к использованию твоего инструмента.
Но как это бывает в реальной жизни, не все инструменты одно кнопочные и у многих большой и сложный функционал. Взять к примеру следующие веб-инструменты:
  1. Topvisor
  2. Google Analytics
Нельзя так просто взять и пользоваться ими. Сначала, ты должен научиться ими пользоваться. В моём случае получился не очень интуитивно понятный инструмент. По этому будем делать встроенный туториал на сайт.

Создание собственного ивента для запуска туториала

У нас уже есть кнопка для вызова цепочки подсказок
Нужно теперь её настроить так, чтобы эта цепочка работала. Сделаем мы это через создание собственного события. В файле Header.js замени:

До

export default function Header() {
const header_buttons = document.getElementById('meta-header')
const btns = []
for ( const btn of header_buttons.children){
const ref = btn.firstElementChild.getAttribute('href')
if (btn.dataset.type == 'inner-link'){
btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><LinkIcon className='mb-2' fontSize='small' /></Button>)
}
else if (btn.dataset.type == 'tutorial' ){
btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><QuestionMarkIcon className='mb-2' fontSize='small'/></Button>)
}

После

export default function Header() {
const header_buttons = document.getElementById('meta-header')
const btns = []
for ( const btn of header_buttons.children){
const ref = btn.firstElementChild.getAttribute('href')
if (btn.dataset.type == 'inner-link'){
btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><LinkIcon className='mb-2' fontSize='small' /></Button>)
}
else if (btn.dataset.type == 'tutorial' ){
btns.push(<Button id='onTutorial' onClick={(e)=>{
const onTutorialEvent = new Event('onTutorial')
e.currentTarget.dispatchEvent(onTutorialEvent)
}} color='primary'><a href={ref}>{btn.innerText}</a><QuestionMarkIcon className='mb-2' fontSize='small'/></Button>)
}

Мы добавили к кнопке айдишник + собственное событие на клик.

Создание нового компонента Tutorial.js

Теперь создадим новый компонент. В директории src/components создай файл Tutorial.js. После чего добавь туда следующий код:
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import Popover from '@mui/material/Popover';


export default function Hint({children, anchor}) {
const [anchorEl, setAnchorEl] = React.useState(null);
const [counter, setCounter] = React.useState(1);
const [message, setMessage] = React.useState('');


const clearAllHints = () => {
const hints = document.querySelectorAll('.tutorial_hint')
hints.forEach( (hint_target) => {
hint_target.style = ''
if (hint_target.classList.contains('to_be_hidden')){
hint_target.classList.add('hidden')
hint_target.classList.remove('to_be_hidden')
}
})
}


const displayNextHint = () => {
const hints = document.querySelectorAll('.tutorial_hint')
hints.forEach( (hint_target) => {
if (parseInt(hint_target.dataset.queue) == counter){
hint_target.style = 'background-color: orange'
if (hint_target.classList.contains('hidden')){
hint_target.classList.remove('hidden')
hint_target.classList.add('to_be_hidden')
}
var hint_msg = document.getElementById(`tutorial_hint_${hint_target.dataset.queue}`)
setMessage(hint_msg.innerText)
setAnchorEl(hint_target)
}
})
setCounter(counter + 1)
}


const handleClick = (event) => {
clearAllHints()
displayNextHint()
};


let tutorial_button = document.getElementById('onTutorial')
tutorial_button.addEventListener('onTutorial', handleClick, {once: true})


const handleClose = () => {
const hints = document.querySelectorAll('.tutorial_hint')
if (hints.length < counter){
setMessage('')
setCounter(1)
setAnchorEl(null);
clearAllHints()
}
else{
clearAllHints()
displayNextHint()
}
};


const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;


return (
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
{message}
</Popover>
);
}


const popover = document.getElementById('popover-tutorial');
const root = createRoot(popover);
root.render(<Hint></Hint>);

Суть данного кода сводиться к тому, чтобы искать помеченные элементы с классом tutorial_hint и отрендерить подсказку рядом с этим элементом. Функцию на которую стоит обратить внимание это displayNextHint. Найдя все помеченные элементы, она находит элемент с ней связанный и берёт от туда текст подсказки. После чего выделяет элемент подсказки и показывает саму подсказку.
Чтобы компонент смог отрендериться добавим ещё один элемент в app.html, прямо перед app_settings элементом.
<div id="popover-tutorial"></div>
И подключим новый компонент в index.js

Помечаем элементы для подсказок

Осталось только пометить необходимые элементы для подсказок.

Файл AppActions.js

export default function AppActions(){
const [isModal, setModal] = React.useState(false);
const req = {'setModal': setModal}
return (
<Box className="flex gap-1">
<div id="tutorial_hint_4" className="hidden"> Optional: Then you can save all of your configurations</div>
<IconButton id='onSaveRequest' onClick={()=>{setModal(true); waitTillModalIsUp(SaveRequest, req)}} className='w-fit tutorial_hint' data-queue="4"><SaveIcon className=" border-2 rounded-md"/></IconButton>
<Modal
open={isModal}
onClose={()=>{setModal(false)}}
>
<Box id='utils-modal' className="absolute top-1/2 left-1/2 -translate-x-2/4 -translate-y-2/4 shadow-md p-4 bg-white">
</Box>
</Modal>
<div id="tutorial_hint_3" className="hidden">Step 3: And now you can gather all informatino from SERP results</div>
<IconButton id='onStartParsing' data-queue="3" onClick={()=>{StartParsingRequest(req)}} className='w-fit tutorial_hint'><NotStartedIcon className=" border-2 rounded-md"/></IconButton>
</Box>
)
}

Файл AppQueries.js

export default function AppQueries(){
return (
<div>
<Popover
id={id}
open={open}
anchorEl={anchorEngine}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
{engine_list}
</List>
</Popover>
<div id="tutorial_hint_2" className="hidden">Step 2: Choose an required engine to parse. And then type your query</div>
<IconButton onClick={(ev)=>handleClick(ev,engine_list)} className='w-fit tutorial_hint' data-queue="2"><AddBoxIcon/></IconButton>
</div>
)
}

Файл AppSettings.js

export default function AppSettings(){
...
return (
<div>
<Paper elevation={2} className="z-10">
<div id="tutorial_hint_1" className="hidden" >Step 1: Check at least one checkbox, to choose what to save</div>
<div id="settings_content" className='hidden'>
<SettingsContent />
</div>
</Paper>
{/* Icon button is gonna be changed */}
<IconButton onClick={ToggleSettings} className='w-fit tutorial_hint' data-queue="1">
{ isSettings ? <CloseIcon className=" border-2 rounded-md"/>
: <SettingsApplicationsIcon className=" border-2 rounded-md"/>}
</IconButton>
</div>
)
}

Файл AppUtils.js

export default function AppUtils(){
...
return (
<Box className="flex flex-col">
<Box className="flex gap-1">
<div id="tutorial_hint_5" className="hidden">Here you can find the most popular and ready-to-use presets</div>
<IconButton onClick={ToggleTrending(true)} className='w-fit border tutorial_hint' data-queue="5"><TrendingUpIcon className=" border-2 rounded-md"/></IconButton>
<div id="tutorial_hint_6" className="hidden">Here you will find your own presets</div>
<IconButton onClick={ToggleOwnSaves(true)} className='w-fit tutorial_hint' data-queue="6"><SavedSearchIcon className=" border-2 rounded-md"/></IconButton>
<div id="tutorial_hint_7" className="hidden">Here you will see the actual proccess of gathering information</div>
<div id="console-button" className='hidden tutorial_hint' data-queue="7"><IconButton onClick={ToggleConsole(true)} className='w-fit'><FeaturedPlayListIcon className=" border-2 rounded-md"/></IconButton></div>
<div id="tutorial_hint_8" className="hidden">It is a link to a results of parsing proccess</div>
<div id="results-button" className='hidden tutorial_hint' data-queue="8"><IconButton id="results-button-ref" href='#' onClick={()=>{}} className='w-fit'><DownloadIcon color='warning' className=" border-2 rounded-md"></DownloadIcon></IconButton></div>
</Box>
...
}

Выводы или то как это выглядит

По итогу всё это будет выглядеть как-то так:
Можно и самому потыкаться на сайте.
Если ты пропустил всё что было выше и хочешь просто получить проект в текущем состоянии, то ты можешь скачать его здесь. В следующей статье мы создадим возможность пользователям, входить и регистрироваться на нашем сайте.

Комментарии

(0)

captcha
Отправить
Сейчас тут пусто. Буть первым (o゚v゚)ノ

Другое

Использованные термины


Релевантные вопросы


Похожие статьи


Разработка фронтенда сайта на React с бэкендом на Django | SearchResultParser ч. 2

Часы
16.08.2024
Показываю и рассказываю о том как разработать фронтен для сайта на Реакте с бэкендом на django. Использую MaterialUI и TailwindCSS, с исходным кодом и комментариями.