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

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

Привет, опять. В этой статье ты узнаешь как быстро и просто создать интерактивный туториал по вашему инструменту на Реакте. И первый вопрос который бы я задал, был бы такой: а нужен ли он вообще?
И действительно, если функционал инструмента максимально простой и интуитивно понятный, то заморачиваться над туториалом не стоит. В идеале, к этому и нужно стремиться чтобы пользователь без всякой лишней мысли приступал к использованию твоего инструмента.
Но как это бывает в реальной жизни, не все инструменты одно кнопочные и у многих большой и сложный функционал. Взять к примеру следующие веб-инструменты:
  • Topvisor
  • 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
AppQueries.js
AppSettings.js
AppUtils.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>
        )
    }
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>
        )
    }
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>
        )
    }
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
3 соединённые точки
0

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

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

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

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

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